Files
pathfinder/app/main/model/basicmodel.php
Mark Friedrich ecd505a202 v1.0.0 (#183)
* #84 test data dump from CREST login

* updated "credits" dialog (Google+ link)
fixed login form layout

* updated Cortex Data-Mapper

* - #84 CREST Login (WIP)
- New CREST controller
- Database restructuring
- improved type-casting for some controller functions
- New login process
- Fixed some bugs during the setup process (/setup root)
- Added CREST request caching by response headers

* pathfinder-84 [Feature Request] CREST Pilot Tracking, many smaller Bugfixes

* pathfinder-84 [Feature Request] added develop JS files

* closed #121 fixed wormhole signature type caching

* closed #120 removed map-loading animation for larger maps (same behaviour as IGB)

* closed #119 fixed wormhole signature id count

* closed #114 Added check for already existing system when adding a new one. (fixed PDO 'duplicate entry' error)

* closed #112 fixed DataTables error for missing "status" data (signature table)

* closed #111 fixed convertDataToUTC(); client side date transformation

* closed #109 fixed system TrueSec rounding

* closed #103 fixed system updated timestamp in getData()

* fixed CSS class for secStatus in Routes module

* closed #121 fixed wormhole signature type caching

* changed dateTime format from German to US format
fixed some minor bugs in signatureTable module

* closed #81 fixed "signature type" overwriting by "signature reader" update

* closed #106 added new signature_types form C5/6 wormholes (gas/ore)

* closed #129 fixed parameter hinting

* closed #131 new "route search" algorithm, added current map systems to live search, added refresh/update functionality for each found route, added bulk route refresh function, added "meta map" route search (search on multiple maps), added route "filters" (restrict search on "stargates", "wormholes", "jumpbridges"), added route "filter" for wormholes (reduced/critical wormholes)
closed #89 fixed "loop connections" on same system
#84 added error messages for "invalid" CREST "Client ID"
added "bootboxjs" (customized styled checkboxes/radio buttons) CSS only
"Font Awesome" version upgrade 4.4.0 -> 4.61
"Bootbox.js" version upgrade 4.3.0 -> 4.4.0
fixed "system dialog" (added responsive layout)

* closed #134  fixed db column type DT_INT (8 bytes) to DT_BIGINT

* closed #138 added new cookie based login

* closed #137 fixed javascript errors on trying to establish an "invalid" connection

* - #84, #138 improved "character selection" on login page (expired cookies are deleted, character panel layout improvements)
- added new "Server info panel" to the login page
- added new cronjob to delete expired cookie authentication data

* #138 enables character switching between characters which have same user

* - PHP Framework upgrade 3.5.0 -> 3.5.1 (fixes some issues with CREST cURL caching, and SESSION management)
- #138 added "cookie logout" to "logout" menu entry

* - updated "feature page" with new feature descriptions and label
- added some new images to the "feature gallery"
- removed "beta" status from "magnetizing" feature on map menu
- hide "server status" panel on "mobile" breakpoint

* - #138 clear character authentication data on sold characters

* closed #142 added custom "onsuspect()" session handler

* #142 do not log suspect if no file is defined in pathfinder.ini

* #142 added NullSec Data/Relic sites to C1/2/3 wormholes as signature option

* #144 fixed "Character not found" warning

* #144 fixed "Character not found" warning

* closed #144 fixed broken routes panel in IGB

* updated README.md for upcoming release

* #147 response header validation

* #149 changed comment for 'BASE' framework var

* fixed map  import

* - added minimal SDE dump (EVE Online: Citadel)
- #147 improved CREST API error logging (WIP)
- improved SSO controller (removed access_token from public endpoints)

* closed #154 added alliance maps to CREST API

* - updated Gulp build dependencies
- increased CREST timeout from 3s -> 4s
- added "Accept" Headers for some CREST endpoints

* cloased #147

* - closed #153 added character verification check for getAll(); Signatures Ajax endpoint

* - updated README.md (added Slack developer chat information)

* Bugfix frig holes (#159)

* added missing frigate wormholes and fixed Q003 destination in shattered wormholes

* changed C7 to 0.0 for Q003

* - fixed broken "graph" data for system

* added a  "failover" system  for bad crest requests (HTTP status 5xx,.. )

* Red Gaint => Red Giant (#161)

* closed #163 added CREST endpoint support for "waypoints"

* fixed typo

* closed #160 fixed tooltip container

* - added new features to login page

* closes #154 added alliance map support

* fixed XML path for cronjobs

* fixed a bug with inactive "private" maps

* closes #175 added alternative environment configuration

* - v1.0.0  build
2016-06-03 23:05:34 +02:00

664 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Created by PhpStorm.
* User: exodus4d
* Date: 16.02.15
* Time: 22:11
*/
namespace Model;
use DB\SQL\Schema;
use Exception;
use Controller;
use DB;
abstract class BasicModel extends \DB\Cortex {
/**
* Hive key with DB object
* @var string
*/
protected $db = 'DB_PF';
/**
* caching time of field schema - seconds
* as long as we don´t expect any field changes
* -> leave this at a higher value
* @var int
*/
//protected $ttl = 86400;
/**
* 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;
/**
* field validation array
* @var array
*/
protected $validate = [];
/**
* 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;
public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){
$this->addStaticFieldConfig();
parent::__construct($db, $table, $fluid, $ttl);
// events -----------------------------------------
$this->afterinsert(function($self){
$self->clearCacheData();
});
$this->afterupdate( function($self){
$self->clearCacheData();
});
$this->beforeinsert( function($self){
$self->beforeInsertEvent($self);
});
$this->beforeerase( function($self){
$self->beforeeraseEvent($self);
});
$this->aftererase( function($self){
$self->aftereraseEvent($self);
});
}
/**
* @param string $key
* @param mixed $val
* @return mixed|void
* @throws Exception\ValidationException
*/
public function set($key, $val){
if($key == 'active'){
// prevent abuse
return;
}
if(
!$this->dry() &&
$key != 'updated'
){
if( $this->exists($key) ){
$currentVal = $this->get($key);
// if current value is not a relational object
// and value has changed -> update table col
if(is_object($currentVal)){
if(
is_numeric($val) &&
is_subclass_of($currentVal, 'Model\BasicModel') &&
$currentVal->_id !== (int)$val
){
$this->touch('updated');
}
}elseif($currentVal != $val){
$this->touch('updated');
}
}
}
// trim all values
if(is_string($val)){
$val = trim($val);
}
$valid = $this->validateField($key, $val);
if(!$valid){
$this->throwValidationError($key);
}else{
return parent::set($key, $val);
}
}
/**
* extent the fieldConf Array with static fields for each table
*/
private function addStaticFieldConfig(){
// add static fields to this mapper
// 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
]
];
$this->fieldConf = array_merge($staticFieldConfig, $this->fieldConf);
}
}
/**
* validates a table column based on validation settings
* @param $col
* @param $val
* @return bool
*/
private function validateField($col, $val){
$valid = true;
if(array_key_exists($col, $this->validate)){
$fieldValidationOptions = $this->validate[$col];
foreach($fieldValidationOptions as $validateKey => $validateOption ){
if(is_array($fieldValidationOptions[$validateKey])){
$fieldSubValidationOptions = $fieldValidationOptions[$validateKey];
foreach($fieldSubValidationOptions as $validateSubKey => $validateSubOption ){
switch($validateKey){
case 'length':
switch($validateSubKey){
case 'min';
if(strlen($val) < $validateSubOption){
$valid = false;
}
break;
case 'max';
if(strlen($val) > $validateSubOption){
$valid = false;
}
break;
}
break;
}
}
}else{
switch($validateKey){
case 'regex':
$valid = (bool)preg_match($fieldValidationOptions[$validateKey], $val);
break;
}
}
// a validation rule failed
if(!$valid){
break;
}
}
}
return $valid;
}
/**
* 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($dataCacheTableKeyPrefix = ''){
$cacheKey = null;
// set a model unique cache key if the model is saved
if( $this->id > 0){
// 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
$cacheKey = $this->dataCacheKeyPrefix;
$cacheKey .= '.' . strtoupper($this->table);
if($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 \stdClass|null
*/
protected function getCacheData($dataCacheKeyPrefix = ''){
$cacheKey = $this->getCacheKey($dataCacheKeyPrefix);
$cacheData = null;
if( !is_null($cacheKey) ){
$f3 = self::getF3();
if( $f3->exists($cacheKey) ){
$cacheData = $f3->get( $cacheKey );
}
}
return $cacheData;
}
/**
* update/set the getData() cache for this object
* @param $cacheData
* @param string $dataCacheKeyPrefix
* @param int $data_ttl
*/
public function updateCacheData($cacheData, $dataCacheKeyPrefix = '', $data_ttl = 300){
$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
*/
public function clearCacheData(){
$cacheKey = $this->getCacheKey();
if( !is_null($cacheKey) ){
$f3 = self::getF3();
if( $f3->exists($cacheKey) ){
$f3->clear($cacheKey);
}
}
}
/**
* Throws a validation error for a giben column
* @param $col
* @throws \Exception\ValidationException
*/
protected function throwValidationError($col){
throw new Exception\ValidationException('Validation failed: "' . $col . '".', $col);
}
/**
* set "updated" field to current timestamp
* this is useful to mark a row as "changed"
*/
protected function setUpdated(){
if($this->_id > 0){
$pfDB = DB\Database::instance()->getDB('PF');
$pfDB->exec(
["UPDATE " . $this->table . " SET updated=NOW() WHERE id=:id"],
[
[':id' => $this->_id]
]
);
}
}
/**
* get single dataSet by id
* @param $id
* @param int $ttl
* @return \DB\Cortex
*/
public function getById($id, $ttl = 3) {
return $this->getByForeignKey('id', (int)$id, ['limit' => 1], $ttl);
}
/**
* checks weather this model is active or not
* each model should have an "active" column
* @return bool
*/
public function isActive(){
return (bool)$this->active;
}
/**
* set active state for a model
* @param $value
*/
public function setActive($value){
$this->set('active', (int)$value);
}
/**
* get dataSet by foreign column (single result)
* @param $key
* @param $value
* @param array $options
* @param int $ttl
* @return \DB\Cortex
*/
public function getByForeignKey($key, $value, $options = [], $ttl = 60){
$querySet = [];
$query = [];
if($this->exists($key)){
$query[] = $key . " = :" . $key;
$querySet[':' . $key] = $value;
}
// check active column
if($this->exists('active')){
$query[] = "active = :active";
$querySet[':active'] = 1;
}
array_unshift($querySet, implode(' AND ', $query));
return $this->load( $querySet, $options, $ttl );
}
/**
* Event "Hook" function
* can be overwritten
* return false will stop any further action
*/
public function beforeInsertEvent(){
return true;
}
/**
* Event "Hook" function
* can be overwritten
* @return bool
*/
public function beforeeraseEvent($self){
return true;
}
/**
* Event "Hook" function
* can be overwritten
* @return bool
*/
public function aftereraseEvent($self){
return true;
}
/**
* function should be overwritten in child classes with access restriction
* @param CharacterModel $characterModel
* @return bool
*/
public function hasAccess(CharacterModel $characterModel){
return true;
}
/**
* function should be overwritten in parent classes
* @return bool
*/
public function isValid(){
return true;
}
/**
* export and download table data as *.csv
* this is primarily used for static tables
* @return bool
*/
public function exportData(){
$status = false;
if(static::$enableDataExport){
$tableModifier = static::getTableModifier();
$headers = $tableModifier->getCols();
// just get the records with existing columns
// -> no "virtual" fields or "new" columns
$this->fields($headers);
$allRecords = $this->find();
if($allRecords){
$tableData = $allRecords->castAll();
// 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;
}
/**
* import table data from a *.csv file
* @return bool
*/
public function importData(){
$status = false;
if(static::$enableDataImport){
$filePath = 'export/sql/' . $this->getTable() . '.csv';
if(is_file($filePath)){
$handle = @fopen($filePath, 'r');
$keys = array_map('lcfirst', array_filter(fgetcsv($handle, 0, ';')));
if(count($keys) > 0){
$tableData = [];
while (!feof($handle)) {
$tableData[] = array_combine($keys, array_filter(fgetcsv($handle, 0, ';')));
}
// import row data
$status = $this->importStaticData($tableData);
$this->getF3()->status(202);
}else{
$this->getF3()->error(502, 'File could not be read');
}
}else{
$this->getF3()->error(404, 'File not found: ' . $filePath);
}
}
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($tableData = []){
$rowIDs = [];
$addedCount = 0;
$updatedCount = 0;
$deletedCount = 0;
foreach($tableData as $rowData){
// search for existing record and update columns
$this->getById($rowData['id']);
if($this->dry()){
$addedCount++;
}else{
$updatedCount++;
}
$this->copyfrom($rowData);
$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 the current class name
* -> namespace not included
* @return string
*/
public static function getClassName(){
$parts = explode('\\', static::class);
return end($parts);
}
/**
* factory for all Models
* @param string $model
* @param int $ttl
* @return BasicModel
* @throws \Exception
*/
public static function getNew($model, $ttl = 86400){
$class = null;
$model = '\\' . __NAMESPACE__ . '\\' . $model;
if(class_exists($model)){
$class = new $model( null, null, null, $ttl );
}else{
throw new \Exception('No model class found');
}
return $class;
}
/**
* get the framework instance (singleton)
* @return \Base
*/
public static function getF3(){
return \Base::instance();
}
/**
* debug log function
* @param $text
* @param string $logFile
*/
public static function log($text, $logFile = null){
$logFile = isset($logFile) ? $logFile : self::getF3()->get('PATHFINDER.LOGFILES.DEBUG');
Controller\LogController::getLogger($logFile)->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 $fields
* @return bool|array
*/
public static function indexExists(array $fields=array()){
$tableModifier = self::getTableModifier();
$df = parent::resolveConfiguration();
$check = false;
$indexKey = $df['table'] . '___' . implode('__', $fields);
$indexList = $tableModifier->listIndex();
if(array_key_exists( $indexKey, $indexList)){
$check = $indexList[$indexKey];
}
return $check;
}
/**
* set a multi-column index for this table
* @param array $fields
* @param bool $unique
* @param int $length
* @return bool
*/
public static function setMultiColumnIndex(array $fields=array(), $unique = false, $length = 20){
$status = false;
$tableModifier = self::getTableModifier();
if( self::indexExists($fields) === false ){
$tableModifier->addIndex($fields, $unique, $length);
$buildStatus = $tableModifier->build();
if($buildStatus === 0){
$status = true;
}
}
return $status;
}
}