diff --git a/app/Controller/Controller.php b/app/Controller/Controllerr.php similarity index 100% rename from app/Controller/Controller.php rename to app/Controller/Controllerr.php diff --git a/app/Controller/controller.php b/app/Controller/controller.php deleted file mode 100644 index 161597da..00000000 --- a/app/Controller/controller.php +++ /dev/null @@ -1,999 +0,0 @@ -template = $template; - } - - /** - * @return string - */ - protected function getTemplate(){ - return $this->template; - } - - /** - * get $f3 base object - * @return \Base - */ - protected function getF3() : \Base { - return \Base::instance(); - } - - /** - * get DB connection - * @param string $alias - * @return SQL|null - */ - protected function getDB(string $alias = 'PF') : ?SQL { - return $this->getF3()->DB->getDB($alias); - } - - /** - * event handler for all "views" - * some global template variables are set in here - * @param \Base $f3 - * @param $params - * @return bool - */ - function beforeroute(\Base $f3, $params) : bool { - // init user session - $this->initSession($f3); - - if($f3->get('AJAX')){ - header('Content-Type: application/json'); - - // send "maintenance" Header -> e.g. before server update - if($modeMaintenance = (int)Config::getPathfinderData('login.mode_maintenance')){ - header('Pf-Maintenance: ' . $modeMaintenance); - } - }else{ - $this->initResource($f3); - - $this->setTemplate(Config::getPathfinderData('view.index')); - - - $f3->set('tplImage', \lib\format\Image::instance()); - } - - return true; - } - - /** - * event handler after routing - * -> render view - * @param \Base $f3 - */ - public function afterroute(\Base $f3){ - // send preload/prefetch headers - $resource = Resource::instance(); - if($resource->getOption('output') === 'header'){ - header($resource->buildHeader(), false); - } - - if($file = $this->getTemplate()){ - // Ajax calls don´t need a page render.. - // this happens on client side - echo \Template::instance()->render($file); - } - } - - /** - * init new Session handler - * @param \Base $f3 - */ - protected function initSession(\Base $f3){ - $session = null; - - if( - $f3->get('SESSION_CACHE') === 'mysql' && - ($db = $f3->DB->getDB('PF')) instanceof SQL - ){ - if(!headers_sent() && session_status() != PHP_SESSION_ACTIVE){ - /** - * callback() for suspect sessions - * @param $session - * @param $sid - * @return bool - */ - $onSuspect = function($session, $sid){ - self::getLogger('SESSION_SUSPECT')->write( sprintf( - self::ERROR_SESSION_SUSPECT, - $sid, - $session->ip(), - $session->agent() - )); - // .. continue with default onSuspect() handler - // -> destroy session - return false; - }; - - new DB\SQL\MySQL\Session($db, 'sessions', true, $onSuspect); - } - } - } - - /** - * init new Resource handler - * @param \Base $f3 - */ - protected function initResource(\Base $f3){ - $resource = Resource::instance(); - $resource->setOption('filePath', [ - 'style' => $f3->get('BASE') . '/public/css/' . Config::getPathfinderData('version'), - 'script' => $f3->get('BASE') . '/public/js/' . Config::getPathfinderData('version'), - 'font' => $f3->get('BASE') . '/public/fonts', - 'document' => $f3->get('BASE') . '/public/templates', - 'image' => $f3->get('BASE') . '/public/img' - ], true); - - $resource->register('style', 'pathfinder'); - - $resource->register('script', 'lib/require'); - $resource->register('script', 'app'); - - $resource->register('font', 'oxygen-regular-webfont'); - $resource->register('font', 'oxygen-bold-webfont'); - $resource->register('font', 'fa-regular-400'); - $resource->register('font', 'fa-solid-900'); - $resource->register('font', 'fa-brands-400'); - - $resource->register('url', self::getEnvironmentData('CCP_SSO_URL'), 'prerender'); - $resource->register('url', Config::getPathfinderData('api.ccp_image_server'), 'dns-prefetch'); - $resource->register('url', '//i.ytimg.com', 'dns-prefetch'); // YouTube tiny embed domain - - $f3->set('tplResource', $resource); - } - - /** - * get cookies "state" information - * -> whether user accepts cookies - * @return bool - */ - protected function getCookieState() : bool { - return (bool)count( $this->getCookieByName(self::COOKIE_NAME_STATE) ); - } - - /** - * search for existing cookies - * -> either a specific cookie by its name - * -> or get multiple cookies by their name (search by prefix) - * @param $cookieName - * @param bool $prefix - * @return array - */ - protected function getCookieByName($cookieName, $prefix = false) : array { - $data = []; - - if(!empty($cookieName)){ - $cookieData = (array)$this->getF3()->get('COOKIE'); - if($prefix === true){ - // look for multiple cookies with same prefix - foreach($cookieData as $name => $value){ - if(strpos($name, $cookieName) === 0){ - $data[$name] = $value; - } - } - }elseif(isset($cookieData[$cookieName])){ - // look for a single cookie - $data[$cookieName] = $cookieData[$cookieName]; - } - } - - return $data; - } - - /** - * set/update logged in cookie by character model - * -> store validation data in DB - * @param Pathfinder\CharacterModel $character - * @throws \Exception - */ - protected function setLoginCookie(Pathfinder\CharacterModel $character){ - if( $this->getCookieState() ){ - $expireSeconds = (int)Config::getPathfinderData('login.cookie_expire'); - $expireSeconds *= 24 * 60 * 60; - - $timezone = $this->getF3()->get('getTimeZone')(); - $expireTime = new \DateTime('now', $timezone); - - // add cookie expire time - $expireTime->add(new \DateInterval('PT' . $expireSeconds . 'S')); - - // unique "selector" -> to facilitate database look-ups (small size) - // -> This is preferable to simply using the database id field, - // which leaks the number of active users on the application - $selector = bin2hex( openssl_random_pseudo_bytes(12) ); - - // generate unique "validator" (strong encryption) - // -> plaintext set to user (cookie), hashed version of this in DB - $size = openssl_cipher_iv_length('aes-256-cbc'); - $validator = bin2hex(openssl_random_pseudo_bytes($size) ); - - // generate unique cookie token - $token = hash('sha256', $validator); - - // get unique cookie name for this character - $name = $character->getCookieName(); - - $authData = [ - 'characterId' => $character, - 'selector' => $selector, - 'token' => $token, - 'expires' => $expireTime->format('Y-m-d H:i:s') - ]; - - $authenticationModel = $character->rel('characterAuthentications'); - $authenticationModel->copyfrom($authData); - $authenticationModel->save(); - - $cookieValue = implode(':', [$selector, $validator]); - - // get cookie name -> save new one OR update existing cookie - $cookieName = 'COOKIE.' . self::COOKIE_PREFIX_CHARACTER . '_' . $name; - $this->getF3()->set($cookieName, $cookieValue, $expireSeconds); - } - } - - /** - * get characters from given cookie data - * -> validate cookie data - * -> validate characters - * -> cf. Sso->requestAuthorization() ( equivalent DB based login) - * - * @param array $cookieData - * @param bool $checkAuthorization - * @return Pathfinder\CharacterModel[] - * @throws \Exception - */ - protected function getCookieCharacters($cookieData = [], $checkAuthorization = true) : array { - $characters = []; - - if( - $this->getCookieState() && - !empty($cookieData) - ){ - /** - * @var $characterAuth Pathfinder\CharacterAuthenticationModel - */ - $characterAuth = Pathfinder\AbstractPathfinderModel::getNew('CharacterAuthenticationModel'); - - $timezone = $this->getF3()->get('getTimeZone')(); - $currentTime = new \DateTime('now', $timezone); - - foreach($cookieData as $name => $value){ - // remove invalid cookies - $invalidCookie = false; - - $data = explode(':', $value); - if(count($data) === 2){ - // cookie data is well formatted - $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]); - - // validate "scope hash" - // -> either "normal" scopes OR "admin" scopes - // "expire data" and "validate token" - if( !$characterAuth->dry() ){ - if( - strtotime($characterAuth->expires) >= $currentTime->getTimestamp() && - hash_equals($characterAuth->token, hash('sha256', $data[1])) - ){ - // cookie information is valid - // -> try to update character information from ESI - // e.g. Corp has changed, this also ensures valid "access_token" - /** - * @var $character Pathfinder\CharacterModel - */ - $updateStatus = $characterAuth->characterId->updateFromESI(); - - if( empty($updateStatus) ){ - // make sure character data is up2date! - // -> this is not the case if e.g. userCharacters was removed "ownerHash" changed... - $character = $characterAuth->rel('characterId'); - $character->getById( $characterAuth->get('characterId', true) ); - - // check ESI scopes - $scopeHash = Util::getHashFromScopes($character->esiScopes); - - if( - $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType()) || - $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType('admin')) - ){ - // check if character still has user (is not the case of "ownerHash" changed - // check if character is still authorized to log in (e.g. corp/ally or config has changed - // -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..) - if( $character->hasUserCharacter() ){ - $authStatus = $character->isAuthorized(); - - if( - $authStatus == 'OK' || - !$checkAuthorization - ){ - $character->virtual( 'authStatus', $authStatus); - } - - $characters[$name] = $character; - } - }else{ - // outdated/invalid ESI scopes - $characterAuth->erase(); - $invalidCookie = true; - } - }else{ - $invalidCookie = true; - } - }else{ - // clear existing authentication data from DB - $characterAuth->erase(); - $invalidCookie = true; - } - }else{ - $invalidCookie = true; - } - $characterAuth->reset(); - }else{ - $invalidCookie = true; - } - - // remove invalid cookie - if($invalidCookie){ - $this->getF3()->clear('COOKIE.' . $name); - } - } - } - - return $characters; - } - - /** - * get current character from session data - * @param int $ttl - * @return Pathfinder\CharacterModel|null - * @throws \Exception - */ - protected function getSessionCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { - $character = null; - if($user = $this->getUser($ttl)){ - $header = self::getRequestHeaders(); - $requestedCharacterId = (int)$header['Pf-Character']; - if( !$this->getF3()->get('AJAX') ){ - $requestedCharacterId = (int)$_COOKIE['old_char_id']; - if(!$requestedCharacterId){ - $tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA); - if((int)$tempCharacterData['ID'] > 0){ - $requestedCharacterId = (int)$tempCharacterData['ID']; - } - } - } - - $character = $user->getSessionCharacter($requestedCharacterId, $ttl); - } - - return $character; - } - - /** - * get current character - * @param int $ttl - * @return Pathfinder\CharacterModel|null - * @throws \Exception - */ - public function getCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { - return $this->getSessionCharacter($ttl); - } - - /** - * get current user - * @param int $ttl - * @return Pathfinder\UserModel|null - * @throws \Exception - */ - public function getUser($ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\UserModel { - $user = null; - - if($this->getF3()->exists(Api\User::SESSION_KEY_USER_ID, $userId)){ - /** - * @var $userModel Pathfinder\UserModel - */ - $userModel = Pathfinder\AbstractPathfinderModel::getNew('UserModel'); - $userModel->getById($userId, $ttl); - - if( - !$userModel->dry() && - $userModel->hasUserCharacters() - ){ - $user = $userModel; - } - } - - return $user; - } - - /** - * set temp login character data (required during HTTP redirects on login) - * @param int $characterId - * @throws \Exception - */ - protected function setTempCharacterData(int $characterId){ - if($characterId > 0){ - $tempCharacterData = [ - 'ID' => $characterId - ]; - $this->getF3()->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $tempCharacterData); - }else{ - throw new \Exception( sprintf(self::ERROR_TEMP_CHARACTER_ID, $characterId) ); - } - } - - /** - * log out current character or all active characters (multiple browser tabs) - * -> send response data to client - * @param \Base $f3 - * @param bool $all - * @param bool $deleteSession - * @param bool $deleteLog - * @param bool $deleteCookie - * @throws \Exception - */ - protected function logoutCharacter(\Base $f3, bool $all = false, bool $deleteSession = true, bool $deleteLog = true, bool $deleteCookie = false){ - $sessionCharacterData = (array)$f3->get(Api\User::SESSION_KEY_CHARACTERS); - - if($sessionCharacterData){ - $activeCharacterId = ($activeCharacter = $this->getCharacter()) ? $activeCharacter->_id : 0; - /** - * @var $character Pathfinder\CharacterModel - */ - $character = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); - $characterIds = []; - foreach($sessionCharacterData as $characterData){ - if($characterData['ID'] === $activeCharacterId){ - $characterIds[] = $activeCharacter->_id; - $activeCharacter->logout($deleteSession, $deleteLog, $deleteCookie); - }elseif($all){ - $character->getById($characterData['ID']); - $characterIds[] = $character->_id; - $character->logout($deleteSession, $deleteLog, $deleteCookie); - } - $character->reset(); - } - - if($characterIds){ - // broadcast logout information to webSocket server - $f3->webSocket()->write('characterLogout', $characterIds); - } - } - - if($f3->get('AJAX')){ - $status = 403; - $f3->status($status); - - $return = (object) []; - $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login'); - $return->error[] = $this->getErrorObject($status, Config::getMessageFromHTTPStatus($status)); - - echo json_encode($return); - }else{ - // redirect to landing page - $f3->reroute(['login']); - } - } - - /** - * get EVE server status from ESI - * @param \Base $f3 - * @throws \Exception - */ - public function getEveServerStatus(\Base $f3){ - $ttl = 60; - $esiStatusVersion = 'latest'; - $cacheKey = 'eve_server_status'; - - if(!$exists = $f3->exists($cacheKey, $return)){ - $return = (object) []; - $return->error = []; - - /** - * @var $client CcpClient - */ - if($client = $f3->ccpClient()){ - $return->server = [ - 'name' => strtoupper(self::getEnvironmentData('CCP_ESI_DATASOURCE')), - 'status' => 'offline', - 'statusColor' => 'red', - ]; - $return->api = [ - 'name' => 'ESI API', - 'status' => 'offline', - 'statusColor' => 'red', - 'url' => $client->getUrl(), - 'timeout' => $client->getTimeout(), - 'connectTimeout' => $client->getConnectTimeout(), - 'readTimeout' => $client->getReadTimeout(), - 'proxy' => ($proxy = $client->getProxy()) ? : 'false', - 'verify' => $client->getVerify(), - 'debug' => $client->getDebugRequests(), - 'dataSource' => $client->getDataSource(), - 'statusVersion' => $esiStatusVersion, - 'routes' => [] - ]; - - $serverStatus = $client->getServerStatus(); - if( !isset($serverStatus['error']) ){ - $statusData = $serverStatus['status']; - // calculate time diff since last server restart - $timezone = $f3->get('getTimeZone')(); - $dateNow = new \DateTime('now', $timezone); - $dateServerStart = new \DateTime($statusData['startTime']); - $interval = $dateNow->diff($dateServerStart); - $startTimestampFormat = $interval->format('%hh %im'); - if($interval->days > 0){ - $startTimestampFormat = $interval->days . 'd ' . $startTimestampFormat; - } - - $statusData['name'] = $return->server['name']; - $statusData['status'] = 'online'; - $statusData['statusColor'] = 'green'; - $statusData['startTime'] = $startTimestampFormat; - $return->server = $statusData; - }else{ - $return->error[] = (new PathfinderException($serverStatus['error'], 500))->getError(); - } - - $apiStatus = $client->getStatusForRoutes('latest'); - if( !isset($apiStatus['error']) ){ - // find top status - $status = 'OK'; - $color = 'green'; - foreach($apiStatus['status'] as &$statusData){ - if('red' == $statusData['status']){ - $status = 'unstable'; - $color = $statusData['status'] = 'orange'; // red is already in use for fatal API errors (e.g. no response at all, or offline) - break; - } - if('yellow' == $statusData['status']){ - $status = 'degraded'; - $color = $statusData['status']; - } - } - - $return->api['status'] = $status; - $return->api['statusColor'] = $color; - $return->api['routes'] = $apiStatus['status']; - }else{ - $return->error[] = (new PathfinderException($apiStatus['error'], 500))->getError(); - } - - if(empty($return->error)){ - $f3->set($cacheKey, $return, $ttl); - } - } - } - - if(empty($return->error)){ - $f3->expire(Config::ttlLeft($exists, $ttl)); - } - - echo json_encode($return); - } - - /** - * @param int $code - * @param string $message - * @param string $status - * @param null $trace - * @return \stdClass - */ - protected function getErrorObject(int $code, string $message = '', string $status = '', $trace = null): \stdClass{ - $object = (object) []; - $object->type = 'error'; - $object->code = $code; - $object->status = empty($status) ? @constant('Base::HTTP_' . $code) : $status; - if(!empty($message)){ - $object->message = $message; - } - if(!empty($trace)){ - $object->trace = $trace; - } - return $object; - } - - /** - * @param string $title - * @param string $message - * @param string $type - * @return \stdClass - */ - protected function getNotificationObject(string $title, $message = '', $type = 'danger') : \stdClass { - $notification = (object) []; - $notification->type = in_array($type, self::NOTIFICATION_TYPES) ? $type : 'danger'; - $notification->title = $title; - $notification->message = $message; - return $notification; - } - - /** - * get a program URL by alias - * -> if no $alias given -> get "default" route (index.php) - * @param null $alias - * @return bool|string - */ - protected function getRouteUrl($alias = null){ - $url = false; - - if(!empty($alias)){ - // check given alias is a valid (registered) route - if(array_key_exists($alias, $this->getF3()->get('ALIASES'))){ - $url = $this->getF3()->alias($alias); - } - }elseif($this->getF3()->get('ALIAS')){ - // get current URL - $url = $this->getF3()->alias( $this->getF3()->get('ALIAS') ); - }else{ - // get main (index.php) URL - $url = $this->getF3()->alias('login'); - } - - return $url; - } - - /** - * get a custom userAgent string for API calls - * @return string - */ - protected function getUserAgent() : string { - $userAgent = ''; - $userAgent .= Config::getPathfinderData('name'); - $userAgent .= ' - ' . Config::getPathfinderData('version'); - $userAgent .= ' | ' . Config::getPathfinderData('contact'); - $userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')'; - - return $userAgent; - } - - /** - * print error information in CLI mode - * @param \stdClass $error - */ - protected function echoErrorCLI(\stdClass $error){ - echo '[' . date('H:i:s') . '] ───────────────────────────' . PHP_EOL; - foreach(get_object_vars($error) as $key => $value){ - $row = str_pad(' ',2 ) . str_pad($key . ':',10 ); - if($key == 'trace'){ - $value = preg_replace("/\r\n|\r|\n/", "\n" . str_pad(' ',12 ), $value); - $row .= PHP_EOL . str_pad(' ',12 ) . $value; - }else{ - $row .= $value; - } - echo $row . PHP_EOL; - } - } - - /** - * onError() callback function - * -> on AJAX request -> return JSON with error information - * -> on HTTP request -> render error page - * @param \Base $f3 - * @return bool - */ - public function showError(\Base $f3){ - - if(!headers_sent()){ - // collect error info ------------------------------------------------------------------------------------- - $errorData = $f3->get('ERROR'); - $exception = $f3->get('EXCEPTION'); - - if($exception instanceof PathfinderException){ - // ... handle Pathfinder exceptions (e.g. validation Exceptions,..) - $error = $exception->getError(); - }else{ - // ... handle error $f3->error() calls - $error = $this->getErrorObject( - $errorData['code'], - $errorData['status'], - $errorData['text'], - $f3->get('DEBUG') >= 1 ? $errorData['trace'] : null - ); - } - - // check if error is a PDO Exception ---------------------------------------------------------------------- - if(strpos(strtolower( $f3->get('ERROR.text') ), 'duplicate') !== false){ - preg_match_all('/\'([^\']+)\'/', $f3->get('ERROR.text'), $matches, PREG_SET_ORDER); - - if(count($matches) === 2){ - $error->field = $matches[1][1]; - $error->message = 'Value "' . $matches[0][1] . '" already exists'; - } - } - - // set response status ------------------------------------------------------------------------------------ - if(!empty($error->code)){ - $f3->status($error->code); - } - - if($f3->get('CLI')){ - $this->echoErrorCLI($error); - // no further processing (no HTML output) - return false; - }elseif($f3->get('AJAX')){ - $return = (object) []; - $return->error[] = $error; - echo json_encode($return); - }else{ - // non AJAX (e.g. GET/POST) - // recursively clear existing output buffers - while(ob_get_level()){ - ob_end_clean(); - } - - $f3->set('tplPageTitle', 'ERROR - ' . $error->code); - // set error data for template rendering - $error->redirectUrl = $this->getRouteUrl(); - $f3->set('errorData', $error); - - // 4xx/5xx error -> set error page template - if( preg_match('/^4[0-9]{2}$/', $error->code) ){ - $f3->set('tplPageContent', Config::getPathfinderData('STATUS.4XX') ); - }elseif( preg_match('/^5[0-9]{2}$/', $error->code) ){ - $f3->set('tplPageContent', Config::getPathfinderData('STATUS.5XX')); - } - - // stop script - die(); after this fkt is done - // -> unload() fkt is still called - $f3->set('HALT', true); - } - } - - return true; - } - - /** - * Callback for framework "unload" - * -> this function is called on each request! - * -> configured in config.ini - * @param \Base $f3 - * @return bool - */ - public function unload(\Base $f3){ - // store all user activities that are buffered for logging in this request - // this should work even on non HTTP200 responses - $this->logActivities(); - - return true; - } - - /** - * store activity log data to DB - */ - protected function logActivities(){ - LogController::instance()->logActivities(); - Monolog::instance()->log(); - } - - /** - * simple counter with "static" store - * -> called within tpl render - * @return \Closure - */ - protected function counter() : \Closure { - $store = []; - - return function(string $action = 'increment', string $type = 'default', $val = 0) use (&$store){ - $return = null; - switch($action){ - case 'increment': $store[$type]++; break; - case 'add': $store[$type] += (int)$val; break; - case 'get': $return = $store[$type] ? : null; break; - case 'reset': unset($store[$type]); break; - } - return $return; - }; - } - - /** - * get controller by class name - * -> controller class is searched within all controller directories - * @param $className - * @return null|Controller - * @throws \Exception - */ - static function getController($className){ - $controller = null; - // add subNamespaces for controller classes - $subNamespaces = ['Api', 'Ccp']; - - for($i = 0; $i <= count($subNamespaces); $i++){ - $path = [__NAMESPACE__]; - $path[] = ( isset($subNamespaces[$i - 1]) ) ? $subNamespaces[$i - 1] : ''; - $path[] = $className; - $classPath = implode('\\', array_filter($path)); - - if(class_exists($classPath)){ - $controller = new $classPath(); - break; - } - } - - if( is_null($controller) ){ - throw new \Exception( sprintf('Controller class "%s" not found!', $className) ); - } - - return $controller; - } - - - /** - * get scope array by a "role" - * @param string $authType - * @return array - */ - static function getScopesByAuthType(string $authType = '') : array { - $scopes = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES')); - switch($authType){ - case 'admin': - $scopesAdmin = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN')); - $scopes = array_merge($scopes, $scopesAdmin); - break; - } - sort($scopes); - return $scopes; - } - - /** - * Helper function to return all headers because - * getallheaders() is not available under nginx - * @return array (string $key -> string $value) - */ - static function getRequestHeaders() : array { - $headers = []; - $headerPrefix = 'http_'; - $prefixLength = mb_strlen($headerPrefix); - $serverData = self::getServerData(); - - if( - function_exists('apache_request_headers') && - $serverData->type === 'apache' - ){ - // Apache WebServer - $headers = apache_request_headers(); - }else{ - // Other WebServer, e.g. Nginx - // Unfortunately this "fallback" does not work for me (Apache) - // Therefore we can´t use this for all servers - // https://github.com/exodus4d/pathfinder/issues/58 - foreach($_SERVER as $name => $value){ - $name = mb_strtolower($name); - if(mb_substr($name, 0, $prefixLength) == $headerPrefix){ - $headers[mb_convert_case(str_replace('_', '-', mb_substr($name, $prefixLength)), MB_CASE_TITLE)] = $value; - } - } - } - - return $headers; - } - - /** - * get some server information - * @param int $ttl cache time (default: 1h) - * @return \stdClass - */ - static function getServerData($ttl = 3600){ - $f3 = \Base::instance(); - $cacheKey = 'PF_SERVER_INFO'; - - if( !$f3->exists($cacheKey) ){ - $serverData = (object) []; - $serverData->type = 'unknown'; - $serverData->version = 'unknown'; - $serverData->requiredVersion = 'unknown'; - $serverData->phpInterfaceType = php_sapi_name(); - - if(strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'nginx' ) !== false){ - // Nginx server - $serverSoftwareArgs = explode('/', strtolower( $_SERVER['SERVER_SOFTWARE']) ); - $serverData->type = reset($serverSoftwareArgs); - $serverData->version = end($serverSoftwareArgs); - $serverData->requiredVersion = $f3->get('REQUIREMENTS.SERVER.NGINX.VERSION'); - }elseif(strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'apache' ) !== false){ - // Apache server - $serverData->type = 'apache'; - $serverData->requiredVersion = $f3->get('REQUIREMENTS.SERVER.APACHE.VERSION'); - - // try to get the apache version... - if(function_exists('apache_get_version')){ - // function does not exists if PHP is running as CGI/FPM module! - $matches = preg_split('/[\s,\/ ]+/', strtolower( apache_get_version() ) ); - if(count($matches) > 1){ - $serverData->version = $matches[1]; - } - } - } - - // cache data for one day - $f3->set($cacheKey, $serverData, $ttl); - } - - return $f3->get($cacheKey); - } - - /** - * get the current registration status - * 0=registration stop |1=new registration allowed - * @return int - */ - static function getRegistrationStatus(){ - return (int)Config::getPathfinderData('registration.status'); - } - - /** - * get a Logger object by Hive key - * -> set in pathfinder.ini - * @param string $type - * @return \Log - */ - static function getLogger($type = 'DEBUG') : \Log { - return LogController::getLogger($type); - } - - /** - * removes illegal characters from a Hive-key that are not allowed - * @param $key - * @return string - */ - static function formatHiveKey($key) : string { - $illegalCharacters = ['-', ' ']; - return strtolower(str_replace($illegalCharacters, '', $key)); - } - - /** - * get environment specific configuration data - * @param string $key - * @return string|array|null - */ - static function getEnvironmentData($key){ - return Config::getEnvironmentData($key); - } - -} \ No newline at end of file diff --git a/app/Model/AbstractModel.php b/app/Model/AbstractModell.php similarity index 100% rename from app/Model/AbstractModel.php rename to app/Model/AbstractModell.php diff --git a/app/Model/abstractmodel.php b/app/Model/abstractmodel.php deleted file mode 100644 index 42411ede..00000000 --- a/app/Model/abstractmodel.php +++ /dev/null @@ -1,1156 +0,0 @@ - leave this at a higher value - * @var int - */ - protected $ttl = 60; - - /** - * caching for relational data - * @var int - */ - protected $rel_ttl = 0; - - /** - * ass static columns for this table - * -> can be overwritten in child models - * @var bool - */ - protected $addStaticFields = true; - - /** - * enables table truncate - * -> see truncate(); - * -> CAUTION! if set to true truncate() will clear ALL rows! - * @var bool - */ - protected $allowTruncate = false; - - /** - * enables change for "active" column - * -> see setActive(); - * -> $this->active = false; will NOT work (prevent abuse)! - * @var bool - */ - private $allowActiveChange = false; - - /** - * getData() cache key prefix - * -> do not change, otherwise cached data is lost - * @var string - */ - private $dataCacheKeyPrefix = 'DATACACHE'; - - /** - * enables data export for this table - * -> can be overwritten in child models - * @var bool - */ - public static $enableDataExport = false; - - /** - * enables data import for this table - * -> can be overwritten in child models - * @var bool - */ - public static $enableDataImport = false; - - /** - * collection for validation errors - * @var array - */ - protected $validationError = []; - - - /** - * default charset for table - */ - const DEFAULT_CHARSET = 'utf8mb4'; - - /** - * default caching time of field schema - seconds - */ - const DEFAULT_TTL = 86400; - - /** - * default TTL for getData(); cache - seconds - */ - const DEFAULT_CACHE_TTL = 120; - - /** - * default TTL or temp table data read from *.csv file - * -> used during data import - */ - const DEFAULT_CACHE_CSV_TTL = 120; - - /** - * cache key prefix name for "full table" indexing - * -> used e.g. for a "search" index; or "import" index for *.csv imports - */ - const CACHE_KEY_PREFIX = 'INDEX'; - - /** - * cache key name for temp data import from *.csv files per table - */ - const CACHE_KEY_CSV_PREFIX = 'CSV'; - - /** - * default TTL for SQL query cache - */ - const DEFAULT_SQL_TTL = 3; - - /** - * data from Universe tables is static and does not change frequently - * -> refresh static data after X days - */ - const CACHE_MAX_DAYS = 60; - - /** - * class not exists error - */ - const ERROR_INVALID_MODEL_CLASS = 'Model class (%s) not found'; - - /** - * AbstractModel constructor. - * @param null $db - * @param null $table - * @param null $fluid - * @param int $ttl - * @param string $charset - */ - public function __construct($db = null, $table = null, $fluid = null, $ttl = self::DEFAULT_TTL, $charset = self::DEFAULT_CHARSET){ - - if(!is_object($db)){ - $db = self::getF3()->DB->getDB(static::DB_ALIAS); - } - - if(is_null($db)){ - // no valid DB connection found -> break on error - self::getF3()->set('HALT', true); - } - - // set charset -> used during table setup() - $this->charset = $charset; - - $this->addStaticFieldConfig(); - - parent::__construct($db, $table, $fluid, $ttl); - - // insert events ------------------------------------------------------------------------------------ - $this->beforeinsert(function($self, $pkeys){ - return $self->beforeInsertEvent($self, $pkeys); - }); - - $this->afterinsert(function($self, $pkeys){ - $self->afterInsertEvent($self, $pkeys); - }); - - // update events ------------------------------------------------------------------------------------ - $this->beforeupdate(function($self, $pkeys){ - return $self->beforeUpdateEvent($self, $pkeys); - }); - - $this->afterupdate(function($self, $pkeys){ - $self->afterUpdateEvent($self, $pkeys); - }); - - // erase events ------------------------------------------------------------------------------------- - $this->beforeerase(function($self, $pkeys){ - return $self->beforeEraseEvent($self, $pkeys); - }); - - $this->aftererase(function($self, $pkeys){ - $self->afterEraseEvent($self, $pkeys); - }); - } - - /** - * checks whether table exists on DB - * @return bool - */ - public function tableExists() : bool { - return is_object($this->db) ? $this->db->tableExists($this->table) : false; - } - - /** - * clear existing table Schema cache - * @return bool - */ - public function clearSchemaCache() : bool { - $f3 = self::getF3(); - $cache=\Cache::instance(); - if( - $f3->CACHE && is_object($this->db) && - $cache->exists($hash = $f3->hash($this->db->getDSN() . $this->table) . '.schema') - ){ - return (bool)$cache->clear($hash); - } - return false; - } - - /** - * @param string $key - * @param mixed $val - * @return mixed - * @throws ValidationException - */ - public function set($key, $val){ - if(is_string($val)){ - $val = trim($val); - } - - if( - !$this->dry() && - $key != 'updated' - ){ - if($this->exists($key)){ - // get raw column data (no objects) - $currentVal = $this->get($key, true); - - if(is_object($val)){ - if( - is_subclass_of($val, 'Model\AbstractModel') && - $val->_id != $currentVal - ){ - // relational object changed - $this->touch('updated'); - } - }elseif($val != $currentVal){ - // non object value - $this->touch('updated'); - } - } - } - - if(!$this->validateField($key, $val)){ - $this->throwValidationException($key); - } - - return parent::set($key, $val); - } - - /** - * setter for "active" status - * -> default: keep current "active" status - * -> can be overwritten - * @param bool $active - * @return mixed - */ - public function set_active($active){ - if($this->allowActiveChange){ - // allowed to set/change -> reset "allowed" property - $this->allowActiveChange = false; - }else{ - // not allowed to set/change -> keep current status - $active = $this->active; - } - return $active; - } - - /** - * get static fields for this model instance - * @return array - */ - protected function getStaticFieldConf() : array { - $staticFieldConfig = []; - - // static tables (fixed data) do not require them... - if($this->addStaticFields){ - $staticFieldConfig = [ - 'created' => [ - 'type' => Schema::DT_TIMESTAMP, - 'default' => Schema::DF_CURRENT_TIMESTAMP, - 'index' => true - ], - 'updated' => [ - 'type' => Schema::DT_TIMESTAMP, - 'default' => Schema::DF_CURRENT_TIMESTAMP, - 'index' => true - ] - ]; - } - - return $staticFieldConfig; - } - - /** - * extent the fieldConf Array with static fields for each table - */ - private function addStaticFieldConfig(){ - $this->fieldConf = array_merge($this->getStaticFieldConf(), $this->fieldConf); - } - - /** - * validates a table column based on validation settings - * @param string $key - * @param $val - * @return bool - */ - protected function validateField(string $key, $val) : bool { - $valid = true; - if($fieldConf = $this->fieldConf[$key]){ - if($method = $this->fieldConf[$key]['validate']){ - if( !is_string($method)){ - $method = $key; - } - $method = 'validate_' . $method; - if(method_exists($this, $method)){ - // validate $key (column) with this method... - $valid = $this->$method($key, $val); - }else{ - self::getF3()->error(501, 'Method ' . get_class($this) . '->' . $method . '() is not implemented'); - }; - } - } - - return $valid; - } - - /** - * validates a model field to be a valid relational model - * @param $key - * @param $val - * @return bool - * @throws ValidationException - */ - protected function validate_notDry($key, $val) : bool { - $valid = true; - if($colConf = $this->fieldConf[$key]){ - if(isset($colConf['belongs-to-one'])){ - if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){ - $valid = true; - }elseif( is_a($val, $colConf['belongs-to-one']) && !$val->dry() ){ - $valid = true; - }else{ - $valid = false; - $msg = 'Validation failed: "' . get_class($this) . '->' . $key . '" must be a valid instance of ' . $colConf['belongs-to-one']; - $this->throwValidationException($key, $msg); - } - } - } - - return $valid; - } - - /** - * validates a model field to be not empty - * @param $key - * @param $val - * @return bool - */ - protected function validate_notEmpty($key, $val) : bool { - $valid = false; - if($colConf = $this->fieldConf[$key]){ - switch($colConf['type']){ - case Schema::DT_INT: - case Schema::DT_FLOAT: - if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){ - $valid = true; - } - break; - case Schema::DT_VARCHAR128: - case Schema::DT_VARCHAR256: - case Schema::DT_VARCHAR512: - if(!empty($val)){ - $valid = true; - } - break; - default: - } - } - - return $valid; - } - - /** - * get key for for all objects in this table - * @return string - */ - private function getTableCacheKey() : string { - return $this->dataCacheKeyPrefix .'.' . strtoupper($this->table); - } - - /** - * get the cache key for this model - * ->do not set a key if the model is not saved! - * @param string $dataCacheTableKeyPrefix - * @return null|string - */ - protected function getCacheKey(string $dataCacheTableKeyPrefix = '') : ?string { - $cacheKey = null; - - // set a model unique cache key if the model is saved - if($this->_id > 0){ - $cacheKey = $this->getTableCacheKey(); - - // check if there is a given key prefix - // -> if not, use the standard key. - // this is useful for caching multiple data sets according to one row entry - if(!empty($dataCacheTableKeyPrefix)){ - $cacheKey .= '.' . $dataCacheTableKeyPrefix . '_'; - }else{ - $cacheKey .= '.ID_'; - } - $cacheKey .= (string)$this->_id; - } - - return $cacheKey; - } - - /** - * get cached data from this model - * @param string $dataCacheKeyPrefix - optional key prefix - * @return mixed|null - */ - protected function getCacheData($dataCacheKeyPrefix = ''){ - $cacheData = null; - // table cache exists - // -> check cache for this row data - if(!is_null($cacheKey = $this->getCacheKey($dataCacheKeyPrefix))){ - self::getF3()->exists($cacheKey, $cacheData); - } - return $cacheData; - } - - /** - * update/set the getData() cache for this object - * @param $cacheData - * @param string $dataCacheKeyPrefix - * @param int $data_ttl - */ - public function updateCacheData($cacheData, string $dataCacheKeyPrefix = '', int $data_ttl = self::DEFAULT_CACHE_TTL){ - $cacheDataTmp = (array)$cacheData; - - // check if data should be cached - // and cacheData is not empty - if( - $data_ttl > 0 && - !empty($cacheDataTmp) - ){ - $cacheKey = $this->getCacheKey($dataCacheKeyPrefix); - if(!is_null($cacheKey)){ - self::getF3()->set($cacheKey, $cacheData, $data_ttl); - } - } - } - - /** - * unset the getData() cache for this object - * -> see also clearCacheDataWithPrefix(), for more information - */ - public function clearCacheData(){ - $this->clearCache($this->getCacheKey()); - } - - /** - * unset object cached data by prefix - * -> primarily used by object cache with multiple data caches - * @param string $dataCacheKeyPrefix - */ - public function clearCacheDataWithPrefix(string $dataCacheKeyPrefix = ''){ - $this->clearCache($this->getCacheKey($dataCacheKeyPrefix)); - } - - /** - * unset object cached data (if exists) - * @param $cacheKey - */ - private function clearCache($cacheKey){ - if(!empty($cacheKey)){ - $f3 = self::getF3(); - if($f3->exists($cacheKey)){ - $f3->clear($cacheKey); - } - } - } - - /** - * throw validation exception for a model property - * @param string $col - * @param string $msg - * @throws ValidationException - */ - protected function throwValidationException(string $col, string $msg = ''){ - $msg = empty($msg) ? 'Validation failed: "' . $col . '".' : $msg; - throw new ValidationException($msg, $col); - } - - /** - * @param string $msg - * @throws DatabaseException - */ - protected function throwDbException(string $msg){ - throw new DatabaseException($msg); - } - - /** - * checks whether this model is active or not - * each model should have an "active" column - * @return bool - */ - public function isActive() : bool { - return (bool)$this->active; - } - - /** - * set active state for a model - * -> do NOT use $this->active for status change! - * -> this will not work (prevent abuse) - * @param bool $active - */ - public function setActive(bool $active){ - // enables "active" change for this model - $this->allowActiveChange = true; - $this->active = $active; - } - - /** - * get single dataSet by id - * @param int $id - * @param int $ttl - * @param bool $isActive - * @return bool - */ - public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool { - return $this->getByForeignKey($this->primary, $id, ['limit' => 1], $ttl, $isActive); - } - - /** - * get dataSet by foreign column (single result) - * @param string $key - * @param $value - * @param array $options - * @param int $ttl - * @param bool $isActive - * @return bool - */ - public function getByForeignKey(string $key, $value, array $options = [], int $ttl = 0, bool $isActive = true) : bool { - $filters = [self::getFilter($key, $value)]; - - if($isActive && $this->exists('active')){ - $filters[] = self::getFilter('active', true); - } - - $this->filterRel(); - - return $this->load($this->mergeFilter($filters), $options, $ttl); - } - - /** - * apply filter() for relations - * -> overwrite in child classes - * @see https://github.com/ikkez/f3-cortex#filter - */ - protected function filterRel() : void {} - - /** - * get first model from a relation that matches $filter - * @param string $key - * @param array $filter - * @return mixed|null - */ - protected function relFindOne(string $key, array $filter){ - $relModel = null; - $relFilter = []; - if($this->exists($key, true)){ - $fieldConf = $this->getFieldConfiguration(); - if(array_key_exists($key, $fieldConf)){ - if(array_key_exists($type = 'has-many', $fieldConf[$key])){ - $fromConf = $fieldConf[$key][$type]; - $relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField'])); - } - } - - /** - * @var $relModel self|bool - */ - $relModel = $this->rel($key)->findone($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); - } - - return $relModel ? : null; - } - - /** - * get all models from a relation that match $filter - * @param string $key - * @param array $filter - * @return CortexCollection|null - */ - protected function relFind(string $key, array $filter) : ?CortexCollection { - $relModel = null; - $relFilter = []; - if($this->exists($key, true)){ - $fieldConf = $this->getFieldConfiguration(); - if(array_key_exists($key, $fieldConf)){ - if(array_key_exists($type = 'has-many', $fieldConf[$key])){ - $fromConf = $fieldConf[$key][$type]; - $relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField'])); - } - } - - /** - * @var $relModel CortexCollection|bool - */ - $relModel = $this->rel($key)->find($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); - } - - return $relModel ? : null; - } - - /** - * Event "Hook" function - * can be overwritten - * return false will stop any further action - * @param self $self - * @param $pkeys - * @return bool - */ - public function beforeInsertEvent($self, $pkeys) : bool { - if($this->exists('updated')){ - $this->touch('updated'); - } - return true; - } - - /** - * Event "Hook" function - * can be overwritten - * return false will stop any further action - * @param self $self - * @param $pkeys - */ - public function afterInsertEvent($self, $pkeys){ - } - - /** - * Event "Hook" function - * can be overwritten - * return false will stop any further action - * @param self $self - * @param $pkeys - * @return bool - */ - public function beforeUpdateEvent($self, $pkeys) : bool { - return true; - } - - /** - * Event "Hook" function - * can be overwritten - * return false will stop any further action - * @param self $self - * @param $pkeys - */ - public function afterUpdateEvent($self, $pkeys){ - } - - /** - * Event "Hook" function - * can be overwritten - * @param self $self - * @param $pkeys - * @return bool - */ - public function beforeEraseEvent($self, $pkeys) : bool { - return true; - } - - /** - * Event "Hook" function - * can be overwritten - * @param self $self - * @param $pkeys - */ - public function afterEraseEvent($self, $pkeys){ - } - - /** - * function should be overwritten in parent classes - * @return bool - */ - public function isValid() : bool { - return true; - } - - /** - * get row count in this table - * @return int - */ - public function getRowCount() : int { - return is_object($this->db) ? $this->db->getRowCount($this->getTable()) : 0; - } - - /** - * truncate all table rows - * -> Use with Caution!!! - */ - public function truncate(){ - if($this->allowTruncate && is_object($this->db)){ - $this->db->exec("TRUNCATE " . $this->getTable()); - } - } - - /** - * format dateTime column - * @param $column - * @param string $format - * @return false|null|string - */ - public function getFormattedColumn($column, $format = 'Y-m-d H:i'){ - return $this->get($column) ? date($format, strtotime( $this->get($column) )) : null; - } - - /** - * export and download table data as *.csv - * this is primarily used for static tables - * @param array $fields - * @return bool - */ - public function exportData(array $fields = []) : bool { - $status = false; - - if(static::$enableDataExport){ - $tableModifier = static::getTableModifier(); - $headers = $tableModifier->getCols(); - - if($fields){ - // columns to export -> reIndex keys - $headers = array_values(array_intersect($headers, $fields)); - } - - // just get the records with existing columns - // -> no "virtual" fields or "new" columns - $this->fields($headers); - $allRecords = $this->find(); - - if($allRecords){ - $tableData = $allRecords->castAll(0); - - // format data -> "id" must be first key - foreach($tableData as &$rowData){ - $rowData = [$this->primary => $rowData['_id']] + $rowData; - unset($rowData['_id']); - } - - $sheet = \Sheet::instance(); - $data = $sheet->dumpCSV($tableData, $headers); - - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Content-Type: text/csv;charset=UTF-8'); - header('Content-Disposition: attachment;filename=' . $this->getTable() . '.csv'); - echo $data; - exit(); - } - } - - return $status; - } - - /** - * read *.csv file for a $table name - * -> 'group' by $getByKey column name and return array - * @param string $table - * @param string $getByKey - * @return array - */ - public static function getCSVData(string $table, string $getByKey = 'id') : array { - $hashKeyTableCSV = static::generateHashKeyTable($table, static::CACHE_KEY_PREFIX . '_' . self::CACHE_KEY_CSV_PREFIX); - - if( - !self::getF3()->exists($hashKeyTableCSV, $tableData) && - !empty($tableData = Util::arrayGetBy(self::loadCSV($table), $getByKey, false)) - ){ - self::getF3()->set($hashKeyTableCSV, $tableData, self::DEFAULT_CACHE_CSV_TTL); - } - - return $tableData; - } - - /** - * load data from *.csv file - * @param string $fileName - * @return array - */ - protected static function loadCSV(string $fileName) : array { - $tableData = []; - - // rtrim(); for arrays (removes empty values) from the end - $rtrim = function($array = [], $lengthMin = false) : array { - $length = key(array_reverse(array_diff($array, ['']), 1))+1; - $length = $length < $lengthMin ? $lengthMin : $length; - return array_slice($array, 0, $length); - }; - - if($fileName){ - $filePath = self::getF3()->get('EXPORT') . 'csv/' . $fileName . '.csv'; - if(is_file($filePath)){ - $handle = @fopen($filePath, 'r'); - $keys = array_map('lcfirst', fgetcsv($handle, 0, ';')); - $keys = $rtrim($keys); - - if(count($keys) > 0){ - while (!feof($handle)) { - $tableData[] = array_combine($keys, $rtrim(fgetcsv($handle, 0, ';'), count($keys))); - } - }else{ - self::getF3()->error(500, 'File could not be read'); - } - }else{ - self::getF3()->error(404, 'File not found: ' . $filePath); - } - } - - return $tableData; - } - - /** - * import table data from a *.csv file - * @return array|bool - */ - public function importData(){ - $status = false; - - if( - static::$enableDataImport && - !empty($tableData = self::loadCSV($this->getTable())) - ){ - // import row data - $status = $this->importStaticData($tableData); - $this->getF3()->status(202); - } - - return $status; - } - - /** - * insert/update static data into this table - * WARNING: rows will be deleted if not part of $tableData ! - * @param array $tableData - * @return array - */ - protected function importStaticData(array $tableData = []) : array { - $rowIDs = []; - $addedCount = 0; - $updatedCount = 0; - $deletedCount = 0; - - $tableModifier = static::getTableModifier(); - $fields = $tableModifier->getCols(); - - foreach($tableData as $rowData){ - // search for existing record and update columns - $this->getById($rowData['id'], 0); - if($this->dry()){ - $addedCount++; - }else{ - $updatedCount++; - } - $this->copyfrom($rowData, $fields); - $this->save(); - $rowIDs[] = $this->_id; - $this->reset(); - } - - // remove old data - $oldRows = $this->find('id NOT IN (' . implode(',', $rowIDs) . ')'); - if($oldRows){ - foreach($oldRows as $oldRow){ - $oldRow->erase(); - $deletedCount++; - } - } - return ['added' => $addedCount, 'updated' => $updatedCount, 'deleted' => $deletedCount]; - } - - /** - * get "default" logging object for this kind of model - * -> can be overwritten - * @param string $action - * @return Logging\LogInterface - */ - protected function newLog(string $action = '') : Logging\LogInterface{ - return new Logging\DefaultLog($action); - } - - /** - * get formatter callback function for parsed logs - * @return null - */ - protected function getLogFormatter(){ - return null; - } - - /** - * add new validation error - * @param ValidationException $e - */ - protected function setValidationError(ValidationException $e){ - $this->validationError[] = $e->getError(); - } - - /** - * get all validation errors - * @return array - */ - public function getErrors() : array { - return $this->validationError; - } - - /** - * checks whether data is outdated and should be refreshed - * @return bool - */ - protected function isOutdated() : bool { - $outdated = true; - if($this->valid()){ - try{ - $timezone = $this->getF3()->get('getTimeZone')(); - $currentTime = new \DateTime('now', $timezone); - $updateTime = \DateTime::createFromFormat( - 'Y-m-d H:i:s', - $this->updated, - $timezone - ); - $interval = $updateTime->diff($currentTime); - if($interval->days < self::CACHE_MAX_DAYS){ - $outdated = false; - } - }catch(\Exception $e){ - self::getF3()->error($e->getCode(), $e->getMessage(), $e->getTrace()); - } - } - return $outdated; - } - - /** - * @return mixed - */ - public function save(){ - $return = false; - try{ - $return = parent::save(); - }catch(ValidationException $e){ - $this->setValidationError($e); - }catch(DatabaseException $e){ - self::getF3()->error($e->getResponseCode(), $e->getMessage(), $e->getTrace()); - } - - return $return; - } - - /** - * @return string - */ - public function __toString() : string { - return $this->getTable(); - } - - /** - * @param string $argument - * @return \ReflectionClass - * @throws \ReflectionException - */ - protected static function refClass($argument = self::class) : \ReflectionClass { - return new \ReflectionClass($argument); - } - - /** - * get the framework instance - * @return \Base - */ - public static function getF3() : \Base { - return \Base::instance(); - } - - /** - * get model data as array - * @param $data - * @return array - */ - public static function toArray($data) : array { - return json_decode(json_encode($data), true); - } - - /** - * get new filter array representation - * -> $suffix can be used fore unique placeholder, - * in case the same $key is used with different $values in the same query - * @param string $key - * @param mixed $value - * @param string $operator - * @param string $suffix - * @return array - */ - public static function getFilter(string $key, $value, string $operator = '=', string $suffix = '') : array { - $placeholder = ':' . implode('_', array_filter([$key, $suffix])); - return [$key . ' ' . $operator . ' ' . $placeholder, $placeholder => $value]; - } - - /** - * stores data direct into the Cache backend (e.g. Redis) - * $f3->set() used the same code. The difference is, that $f3->set() - * also loads data into the Hive. - * This can result in high RAM usage if a great number of key->values should be stored in Cache - * (like the search index for system data) - * @param string $key - * @param $data - * @param int $ttl - */ - public static function setCacheValue(string $key, $data, int $ttl = 0){ - $cache = \Cache::instance(); - $cache->set(self::getF3()->hash($key).'.var', $data, $ttl); - } - - /** - * check whether a cache $key exists - * -> §val (reference) get updated with the cache data - * -> equivalent to $f3->exists() - * @param string $key - * @param null $val - * @return bool - */ - public static function existsCacheValue(string $key, &$val = null){ - $cache = \Cache::instance(); - return $cache->exists(self::getF3()->hash($key).'.var',$val); - } - - /** - * debug log function - * @param string $text - * @param string $type - */ - public static function log($text, $type = 'DEBUG'){ - Controller\LogController::getLogger($type)->write($text); - } - - /** - * get tableModifier class for this table - * @return bool|\DB\SQL\TableModifier - */ - public static function getTableModifier(){ - $df = parent::resolveConfiguration(); - $schema = new Schema($df['db']); - $tableModifier = $schema->alterTable($df['table']); - return $tableModifier; - } - - /** - * Check whether a (multi)-column index exists or not on a table - * related to this model - * @param array $columns - * @return bool|array - */ - public static function indexExists(array $columns = []){ - $tableModifier = self::getTableModifier(); - $df = parent::resolveConfiguration(); - - $check = false; - $indexKey = $df['table'] . '___' . implode('__', $columns); - $indexList = $tableModifier->listIndex(); - if(array_key_exists( $indexKey, $indexList)){ - $check = $indexList[$indexKey]; - } - - return $check; - } - - /** - * set a multi-column index for this table - * @param array $columns Column(s) to be indexed - * @param bool $unique Unique index - * @param int $length index length for text fields in mysql - * @return bool - */ - public static function setMultiColumnIndex(array $columns = [], $unique = false, $length = 20) : bool { - $status = false; - $tableModifier = self::getTableModifier(); - - if( self::indexExists($columns) === false ){ - $tableModifier->addIndex($columns, $unique, $length); - $buildStatus = $tableModifier->build(); - if($buildStatus === 0){ - $status = true; - } - } - - return $status; - } - - /** - * factory for all Models - * @param string $className - * @param int $ttl - * @return AbstractModel|null - * @throws \Exception - */ - public static function getNew(string $className, int $ttl = self::DEFAULT_TTL) : ?self { - $model = null; - $className = self::refClass(static::class)->getNamespaceName() . '\\' . $className; - if(class_exists($className)){ - $model = new $className(null, null, null, $ttl); - }else{ - throw new \Exception(sprintf(self::ERROR_INVALID_MODEL_CLASS, $className)); - } - return $model; - } - - /** - * generate hashKey for a complete table - * -> should hold hashKeys for multiple rows - * @param string $table - * @param string $prefix - * @return string - */ - public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX ) : string { - return $prefix . '_' . strtolower($table); - } - - /** - * overwrites parent - * @param null $db - * @param null $table - * @param null $fields - * @return bool - * @throws \Exception - */ - public static function setup($db = null, $table = null, $fields = null){ - $status = parent::setup($db, $table, $fields); - - // set static default data - if($status === true && property_exists(static::class, 'tableData')){ - $model = self::getNew(self::refClass(static::class)->getShortName(), 0); - $model->importStaticData(static::$tableData); - } - - return $status; - } - -} \ No newline at end of file