From 155cf6aeea9ba7ecbc39face6442d3ce1b03ad8e Mon Sep 17 00:00:00 2001 From: Nils Schwabe Date: Wed, 4 Jun 2014 14:27:03 +0200 Subject: Add webinterface with functionallity --- management-interface/lib/db/cursor.php | 313 +++++++++++++++ management-interface/lib/db/jig.php | 133 +++++++ management-interface/lib/db/jig/mapper.php | 459 +++++++++++++++++++++ management-interface/lib/db/jig/session.php | 168 ++++++++ management-interface/lib/db/mongo.php | 92 +++++ management-interface/lib/db/mongo/mapper.php | 346 ++++++++++++++++ management-interface/lib/db/mongo/session.php | 174 ++++++++ management-interface/lib/db/sql.php | 403 +++++++++++++++++++ management-interface/lib/db/sql/mapper.php | 552 ++++++++++++++++++++++++++ management-interface/lib/db/sql/session.php | 187 +++++++++ 10 files changed, 2827 insertions(+) create mode 100644 management-interface/lib/db/cursor.php create mode 100644 management-interface/lib/db/jig.php create mode 100644 management-interface/lib/db/jig/mapper.php create mode 100644 management-interface/lib/db/jig/session.php create mode 100644 management-interface/lib/db/mongo.php create mode 100644 management-interface/lib/db/mongo/mapper.php create mode 100644 management-interface/lib/db/mongo/session.php create mode 100644 management-interface/lib/db/sql.php create mode 100644 management-interface/lib/db/sql/mapper.php create mode 100644 management-interface/lib/db/sql/session.php (limited to 'management-interface/lib/db') diff --git a/management-interface/lib/db/cursor.php b/management-interface/lib/db/cursor.php new file mode 100644 index 0000000..354c683 --- /dev/null +++ b/management-interface/lib/db/cursor.php @@ -0,0 +1,313 @@ +query[$this->ptr]); + } + + /** + * Return first record (mapper object) that matches criteria + * @return object|FALSE + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function findone($filter=NULL,array $options=NULL,$ttl=0) { + return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; + } + + /** + * Return array containing subset of records matching criteria, + * total number of records in superset, specified limit, number of + * subsets available, and actual subset position + * @return array + * @param $pos int + * @param $size int + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function paginate( + $pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0) { + $total=$this->count($filter,$ttl); + $count=ceil($total/$size); + $pos=max(0,min($pos,$count-1)); + return array( + 'subset'=>$this->find($filter, + array_merge( + $options?:array(), + array('limit'=>$size,'offset'=>$pos*$size) + ), + $ttl + ), + 'total'=>$total, + 'limit'=>$size, + 'count'=>$count, + 'pos'=>$pos<$count?$pos:0 + ); + } + + /** + * Map to first record that matches criteria + * @return array|FALSE + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function load($filter=NULL,array $options=NULL,$ttl=0) { + return ($this->query=$this->find($filter,$options,$ttl)) && + $this->skip(0)?$this->query[$this->ptr=0]:FALSE; + } + + /** + * Map to first record in cursor + * @return mixed + **/ + function first() { + return $this->skip(-$this->ptr); + } + + /** + * Map to last record in cursor + * @return mixed + **/ + function last() { + return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0); + } + + /** + * Map to nth record relative to current cursor position + * @return mixed + * @param $ofs int + **/ + function skip($ofs=1) { + $this->ptr+=$ofs; + return $this->ptr>-1 && $this->ptrquery)? + $this->query[$this->ptr]:FALSE; + } + + /** + * Map next record + * @return mixed + **/ + function next() { + return $this->skip(); + } + + /** + * Map previous record + * @return mixed + **/ + function prev() { + return $this->skip(-1); + } + + /** + * Save mapped record + * @return mixed + **/ + function save() { + return $this->query?$this->update():$this->insert(); + } + + /** + * Delete current record + * @return int|bool + **/ + function erase() { + $this->query=array_slice($this->query,0,$this->ptr,TRUE)+ + array_slice($this->query,$this->ptr,NULL,TRUE); + $this->ptr=0; + } + + /** + * Define onload trigger + * @return callback + * @param $func callback + **/ + function onload($func) { + return $this->trigger['load']=$func; + } + + /** + * Define beforeinsert trigger + * @return callback + * @param $func callback + **/ + function beforeinsert($func) { + return $this->trigger['beforeinsert']=$func; + } + + /** + * Define afterinsert trigger + * @return callback + * @param $func callback + **/ + function afterinsert($func) { + return $this->trigger['afterinsert']=$func; + } + + /** + * Define oninsert trigger + * @return callback + * @param $func callback + **/ + function oninsert($func) { + return $this->afterinsert($func); + } + + /** + * Define beforeupdate trigger + * @return callback + * @param $func callback + **/ + function beforeupdate($func) { + return $this->trigger['beforeupdate']=$func; + } + + /** + * Define afterupdate trigger + * @return callback + * @param $func callback + **/ + function afterupdate($func) { + return $this->trigger['afterupdate']=$func; + } + + /** + * Define onupdate trigger + * @return callback + * @param $func callback + **/ + function onupdate($func) { + return $this->afterupdate($func); + } + + /** + * Define beforeerase trigger + * @return callback + * @param $func callback + **/ + function beforeerase($func) { + return $this->trigger['beforeerase']=$func; + } + + /** + * Define aftererase trigger + * @return callback + * @param $func callback + **/ + function aftererase($func) { + return $this->trigger['aftererase']=$func; + } + + /** + * Define onerase trigger + * @return callback + * @param $func callback + **/ + function onerase($func) { + return $this->aftererase($func); + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->query=array(); + $this->ptr=0; + } + +} diff --git a/management-interface/lib/db/jig.php b/management-interface/lib/db/jig.php new file mode 100644 index 0000000..16f2255 --- /dev/null +++ b/management-interface/lib/db/jig.php @@ -0,0 +1,133 @@ +dir.$file)) + return array(); + $raw=$fw->read($dst); + switch ($this->format) { + case self::FORMAT_JSON: + $data=json_decode($raw,TRUE); + break; + case self::FORMAT_Serialized: + $data=$fw->unserialize($raw); + break; + } + return $data; + } + + /** + * Write data to file + * @return int + * @param $file string + * @param $data array + **/ + function write($file,array $data=NULL) { + $fw=\Base::instance(); + switch ($this->format) { + case self::FORMAT_JSON: + $out=json_encode($data,@constant('JSON_PRETTY_PRINT')); + break; + case self::FORMAT_Serialized: + $out=$fw->serialize($data); + break; + } + return $fw->write($this->dir.$file,$out); + } + + /** + * Return directory + * @return string + **/ + function dir() { + return $this->dir; + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return SQL profiler results + * @return string + **/ + function log() { + return $this->log; + } + + /** + * Jot down log entry + * @return NULL + * @param $frame string + **/ + function jot($frame) { + if ($frame) + $this->log.=date('r').' '.$frame.PHP_EOL; + } + + /** + * Clean storage + * @return NULL + **/ + function drop() { + if ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) + foreach ($glob as $file) + @unlink($file); + } + + /** + * Instantiate class + * @param $dir string + * @param $format int + **/ + function __construct($dir,$format=self::FORMAT_JSON) { + if (!is_dir($dir)) + mkdir($dir,\Base::MODE,TRUE); + $this->uuid=\Base::instance()->hash($this->dir=$dir); + $this->format=$format; + } + +} diff --git a/management-interface/lib/db/jig/mapper.php b/management-interface/lib/db/jig/mapper.php new file mode 100644 index 0000000..3ac3d21 --- /dev/null +++ b/management-interface/lib/db/jig/mapper.php @@ -0,0 +1,459 @@ +document); + } + + /** + * Assign value to field + * @return scalar|FALSE + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + return ($key=='_id')?FALSE:($this->document[$key]=$val); + } + + /** + * Retrieve value of field + * @return scalar|FALSE + * @param $key string + **/ + function get($key) { + if ($key=='_id') + return $this->id; + if (array_key_exists($key,$this->document)) + return $this->document[$key]; + user_error(sprintf(self::E_Field,$key)); + return FALSE; + } + + /** + * Delete field + * @return NULL + * @param $key string + **/ + function clear($key) { + if ($key!='_id') + unset($this->document[$key]); + } + + /** + * Convert array to mapper object + * @return object + * @param $id string + * @param $row array + **/ + protected function factory($id,$row) { + $mapper=clone($this); + $mapper->reset(); + $mapper->id=$id; + foreach ($row as $field=>$val) + $mapper->document[$field]=$val; + $mapper->query=array(clone($mapper)); + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return $obj->document+array('_id'=>$this->id); + } + + /** + * Convert tokens in string expression to variable names + * @return string + * @param $str string + **/ + function token($str) { + $self=$this; + $str=preg_replace_callback( + '/(?stringify(substr($expr[1],1)): + (preg_match('/^\w+/', + $mix=$self->token($expr[2]))? + $fw->stringify($mix): + $mix)). + ']'; + }, + $token[1] + ); + }, + $str + ); + return trim($str); + } + + /** + * Return records that match criteria + * @return array|FALSE + * @param $filter array + * @param $options array + * @param $ttl int + * @param $log bool + **/ + function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { + if (!$options) + $options=array(); + $options+=array( + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ); + $fw=\Base::instance(); + $cache=\Cache::instance(); + $db=$this->db; + $now=microtime(TRUE); + $data=array(); + if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists( + $hash=$fw->hash($this->db->dir(). + $fw->stringify(array($filter,$options))).'.jig',$data)) || + $cached[0]+$ttlread($this->file); + if (is_null($data)) + return FALSE; + foreach ($data as $id=>&$doc) { + $doc['_id']=$id; + unset($doc); + } + if ($filter) { + if (!is_array($filter)) + return FALSE; + // Normalize equality operator + $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]); + // Prepare query arguments + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:array(1=>$args); + $keys=$vals=array(); + $tokens=array_slice( + token_get_all('token($expr)),1); + $data=array_filter($data, + function($_row) use($fw,$args,$tokens) { + $_expr=''; + $ctr=0; + $named=FALSE; + foreach ($tokens as $token) { + if (is_string($token)) + if ($token=='?') { + // Positional + $ctr++; + $key=$ctr; + } + else { + if ($token==':') + $named=TRUE; + else + $_expr.=$token; + continue; + } + elseif ($named && + token_name($token[0])=='T_STRING') { + $key=':'.$token[1]; + $named=FALSE; + } + else { + $_expr.=$token[1]; + continue; + } + $_expr.=$fw->stringify( + is_string($args[$key])? + addcslashes($args[$key],'\''): + $args[$key]); + } + // Avoid conflict with user code + unset($fw,$tokens,$args,$ctr,$token,$key,$named); + extract($_row); + // Evaluate pseudo-SQL expression + return eval('return '.$_expr.';'); + } + ); + } + if (isset($options['order'])) { + $cols=$fw->split($options['order']); + uasort( + $data, + function($val1,$val2) use($cols) { + foreach ($cols as $col) { + $parts=explode(' ',$col,2); + $order=empty($parts[1])? + SORT_ASC: + constant($parts[1]); + $col=$parts[0]; + if (!array_key_exists($col,$val1)) + $val1[$col]=NULL; + if (!array_key_exists($col,$val2)) + $val2[$col]=NULL; + list($v1,$v2)=array($val1[$col],$val2[$col]); + if ($out=strnatcmp($v1,$v2)* + (($order==SORT_ASC)*2-1)) + return $out; + } + return 0; + } + ); + } + $data=array_slice($data, + $options['offset'],$options['limit']?:NULL,TRUE); + if ($fw->get('CACHE') && $ttl) + // Save to cache backend + $cache->set($hash,$data,$ttl); + } + $out=array(); + foreach ($data as $id=>&$doc) { + unset($doc['_id']); + $out[]=$this->factory($id,$doc); + unset($doc); + } + if ($log && isset($args)) { + if ($filter) + foreach ($args as $key=>$val) { + $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [find] '. + ($filter?preg_replace($keys,$vals,$filter[0],1):'')); + } + return $out; + } + + /** + * Count records that match criteria + * @return int + * @param $filter array + * @param $ttl int + **/ + function count($filter=NULL,$ttl=0) { + $now=microtime(TRUE); + $out=count($this->find($filter,NULL,$ttl,FALSE)); + $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [count] '.($filter?json_encode($filter):'')); + return $out; + } + + /** + * Return record at specified offset using criteria of previous + * load() call and make it active + * @return array + * @param $ofs int + **/ + function skip($ofs=1) { + $this->document=($out=parent::skip($ofs))?$out->document:array(); + $this->id=$out?$out->id:NULL; + if ($this->document && isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return array + **/ + function insert() { + if ($this->id) + return $this->update(); + $db=$this->db; + $now=microtime(TRUE); + while (($id=uniqid(NULL,TRUE)) && + ($data=$db->read($this->file)) && isset($data[$id]) && + !connection_aborted()) + usleep(mt_rand(0,100)); + $this->id=$id; + $data[$id]=$this->document; + $pkey=array('_id'=>$this->id); + if (isset($this->trigger['beforeinsert'])) + \Base::instance()->call($this->trigger['beforeinsert'], + array($this,$pkey)); + $db->write($this->file,$data); + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [insert] '.json_encode($this->document)); + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + array($this,$pkey)); + $this->load(array('@_id=?',$this->id)); + return $this->document; + } + + /** + * Update current record + * @return array + **/ + function update() { + $db=$this->db; + $now=microtime(TRUE); + $data=$db->read($this->file); + $data[$this->id]=$this->document; + if (isset($this->trigger['beforeupdate'])) + \Base::instance()->call($this->trigger['beforeupdate'], + array($this,array('_id'=>$this->id))); + $db->write($this->file,$data); + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [update] '.json_encode($this->document)); + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + array($this,array('_id'=>$this->id))); + return $this->document; + } + + /** + * Delete current record + * @return bool + * @param $filter array + **/ + function erase($filter=NULL) { + $db=$this->db; + $now=microtime(TRUE); + $data=$db->read($this->file); + if ($filter) { + foreach ($this->find($filter,NULL,FALSE) as $mapper) + if (!$mapper->erase()) + return FALSE; + return TRUE; + } + elseif (isset($this->id)) { + $pkey=array('_id'=>$this->id); + unset($data[$this->id]); + parent::erase(); + $this->skip(0); + } + else + return FALSE; + if (isset($this->trigger['beforeerase'])) + \Base::instance()->call($this->trigger['beforeerase'], + array($this,$pkey)); + $db->write($this->file,$data); + if ($filter) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:array(1=>$args); + foreach ($args as $key=>$val) { + $vals[]=\Base::instance()-> + stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + } + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [erase] '. + ($filter?preg_replace($keys,$vals,$filter[0],1):'')); + if (isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + array($this,$pkey)); + return TRUE; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->id=NULL; + $this->document=array(); + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $key string + * @param $func callback + **/ + function copyfrom($key,$func=NULL) { + $var=\Base::instance()->get($key); + if ($func) + $var=$func($var); + foreach ($var as $key=>$val) + $this->document[$key]=$val; + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->document as $key=>$field) + $var[$key]=$field; + } + + /** + * Return field names + * @return array + **/ + function fields() { + return array_keys($this->document); + } + + /** + * Instantiate class + * @return void + * @param $db object + * @param $file string + **/ + function __construct(\DB\Jig $db,$file) { + $this->db=$db; + $this->file=$file; + $this->reset(); + } + +} diff --git a/management-interface/lib/db/jig/session.php b/management-interface/lib/db/jig/session.php new file mode 100644 index 0000000..705cbce --- /dev/null +++ b/management-interface/lib/db/jig/session.php @@ -0,0 +1,168 @@ +sid) + $this->load(array('@session_id=?',$this->sid=$id)); + return $this->dry()?FALSE:$this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write($id,$data) { + $fw=\Base::instance(); + $sent=headers_sent(); + $headers=$fw->get('HEADERS'); + if ($id!=$this->sid) + $this->load(array('@session_id=?',$this->sid=$id)); + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('csrf',$sent?$this->csrf():$csrf); + $this->set('ip',$fw->get('IP')); + $this->set('agent', + isset($headers['User-Agent'])?$headers['User-Agent']:''); + $this->set('stamp',time()); + $this->save(); + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id) { + $this->erase(array('@session_id=?',$id)); + setcookie(session_name(),'',strtotime('-1 year')); + unset($_COOKIE[session_name()]); + header_remove('Set-Cookie'); + return TRUE; + } + + /** + * Garbage collector + * @return TRUE + * @param $max int + **/ + function cleanup($max) { + $this->erase(array('@stamp+?dry()?FALSE:$this->get('csrf'); + } + + /** + * Return IP address + * @return string|FALSE + **/ + function ip() { + return $this->dry()?FALSE:$this->get('ip'); + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string|FALSE + **/ + function agent() { + return $this->dry()?FALSE:$this->get('agent'); + } + + /** + * Instantiate class + * @param $db object + * @param $table string + **/ + function __construct(\DB\Jig $db,$table='sessions') { + parent::__construct($db,'sessions'); + session_set_save_handler( + array($this,'open'), + array($this,'close'), + array($this,'read'), + array($this,'write'), + array($this,'destroy'), + array($this,'cleanup') + ); + register_shutdown_function('session_commit'); + @session_start(); + $fw=\Base::instance(); + $headers=$fw->get('HEADERS'); + if (($ip=$this->ip()) && $ip!=$fw->get('IP') || + ($agent=$this->agent()) && + (!isset($headers['User-Agent']) || + $agent!=$headers['User-Agent'])) { + session_destroy(); + $fw->error(403); + } + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + if ($this->load(array('@session_id=?',$this->sid=session_id()))) { + $this->set('csrf',$csrf); + $this->save(); + } + } + +} diff --git a/management-interface/lib/db/mongo.php b/management-interface/lib/db/mongo.php new file mode 100644 index 0000000..833f160 --- /dev/null +++ b/management-interface/lib/db/mongo.php @@ -0,0 +1,92 @@ +dsn; + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return MongoDB profiler results + * @return string + **/ + function log() { + $cursor=$this->selectcollection('system.profile')->find(); + foreach (iterator_to_array($cursor) as $frame) + if (!preg_match('/\.system\..+$/',$frame['ns'])) + $this->log.=date('r',$frame['ts']->sec).' ('. + sprintf('%.1f',$frame['millis']).'ms) '. + $frame['ns'].' ['.$frame['op'].'] '. + (empty($frame['query'])? + '':json_encode($frame['query'])). + (empty($frame['command'])? + '':json_encode($frame['command'])). + PHP_EOL; + return $this->log; + } + + /** + * Intercept native call to re-enable profiler + * @return int + **/ + function drop() { + $out=parent::drop(); + $this->setprofilinglevel(2); + return $out; + } + + /** + * Instantiate class + * @param $dsn string + * @param $dbname string + * @param $options array + **/ + function __construct($dsn,$dbname,array $options=NULL) { + $this->uuid=\Base::instance()->hash($this->dsn=$dsn); + $class=class_exists('\MongoClient')?'\MongoClient':'\Mongo'; + parent::__construct(new $class($dsn,$options?:array()),$dbname); + $this->setprofilinglevel(2); + } + +} diff --git a/management-interface/lib/db/mongo/mapper.php b/management-interface/lib/db/mongo/mapper.php new file mode 100644 index 0000000..bcb7f6e --- /dev/null +++ b/management-interface/lib/db/mongo/mapper.php @@ -0,0 +1,346 @@ +document); + } + + /** + * Assign value to field + * @return scalar|FALSE + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + return $this->document[$key]=$val; + } + + /** + * Retrieve value of field + * @return scalar|FALSE + * @param $key string + **/ + function get($key) { + if ($this->exists($key)) + return $this->document[$key]; + user_error(sprintf(self::E_Field,$key)); + return FALSE; + } + + /** + * Delete field + * @return NULL + * @param $key string + **/ + function clear($key) { + unset($this->document[$key]); + } + + /** + * Convert array to mapper object + * @return object + * @param $row array + **/ + protected function factory($row) { + $mapper=clone($this); + $mapper->reset(); + foreach ($row as $key=>$val) + $mapper->document[$key]=$val; + $mapper->query=array(clone($mapper)); + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return $obj->document; + } + + /** + * Build query and execute + * @return array + * @param $fields string + * @param $filter array + * @param $options array + * @param $ttl int + **/ + function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { + if (!$options) + $options=array(); + $options+=array( + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ); + $fw=\Base::instance(); + $cache=\Cache::instance(); + if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). + $fw->stringify(array($fields,$filter,$options))).'.mongo', + $result)) || !$ttl || $cached[0]+$ttlcollection->group( + $options['group']['keys'], + $options['group']['initial'], + $options['group']['reduce'], + array( + 'condition'=>$filter, + 'finalize'=>$options['group']['finalize'] + ) + ); + $tmp=$this->db->selectcollection( + $fw->get('HOST').'.'.$fw->get('BASE').'.'. + uniqid(NULL,TRUE).'.tmp' + ); + $tmp->batchinsert($grp['retval'],array('safe'=>TRUE)); + $filter=array(); + $collection=$tmp; + } + else { + $filter=$filter?:array(); + $collection=$this->collection; + } + $this->cursor=$collection->find($filter,$fields?:array()); + if ($options['order']) + $this->cursor=$this->cursor->sort($options['order']); + if ($options['limit']) + $this->cursor=$this->cursor->limit($options['limit']); + if ($options['offset']) + $this->cursor=$this->cursor->skip($options['offset']); + $result=array(); + while ($this->cursor->hasnext()) + $result[]=$this->cursor->getnext(); + if ($options['group']) + $tmp->drop(); + if ($fw->get('CACHE') && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + $out=array(); + foreach ($result as $doc) + $out[]=$this->factory($doc); + return $out; + } + + /** + * Return records that match criteria + * @return array + * @param $filter array + * @param $options array + * @param $ttl int + **/ + function find($filter=NULL,array $options=NULL,$ttl=0) { + if (!$options) + $options=array(); + $options+=array( + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ); + return $this->select(NULL,$filter,$options,$ttl); + } + + /** + * Count records that match criteria + * @return int + * @param $filter array + * @param $ttl int + **/ + function count($filter=NULL,$ttl=0) { + $fw=\Base::instance(); + $cache=\Cache::instance(); + if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( + array($filter))).'.mongo',$result)) || !$ttl || + $cached[0]+$ttlcollection->count($filter); + if ($fw->get('CACHE') && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + return $result; + } + + /** + * Return record at specified offset using criteria of previous + * load() call and make it active + * @return array + * @param $ofs int + **/ + function skip($ofs=1) { + $this->document=($out=parent::skip($ofs))?$out->document:array(); + if ($this->document && isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return array + **/ + function insert() { + if (isset($this->document['_id'])) + return $this->update(); + if (isset($this->trigger['beforeinsert'])) + \Base::instance()->call($this->trigger['beforeinsert'], + array($this,$pkey)); + $this->collection->insert($this->document); + $pkey=array('_id'=>$this->document['_id']); + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + array($this,$pkey)); + $this->load(array('_id'=>$this->document['_id'])); + return $this->document; + } + + /** + * Update current record + * @return array + **/ + function update() { + if (isset($this->trigger['beforeupdate'])) + \Base::instance()->call($this->trigger['beforeupdate'], + array($this,$pkey)); + $this->collection->update( + $pkey=array('_id'=>$this->document['_id']), + $this->document, + array('upsert'=>TRUE) + ); + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + array($this,$pkey)); + return $this->document; + } + + /** + * Delete current record + * @return bool + * @param $filter array + **/ + function erase($filter=NULL) { + if ($filter) + return $this->collection->remove($filter); + $pkey=array('_id'=>$this->document['_id']); + if (isset($this->trigger['beforeerase'])) + \Base::instance()->call($this->trigger['beforeerase'], + array($this,$pkey)); + $result=$this->collection-> + remove(array('_id'=>$this->document['_id'])); + parent::erase(); + $this->skip(0); + if (isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + array($this,$pkey)); + return $result; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->document=array(); + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $key string + * @param $func callback + **/ + function copyfrom($key,$func=NULL) { + $var=\Base::instance()->get($key); + if ($func) + $var=$func($var); + foreach ($var as $key=>$val) + $this->document[$key]=$val; + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->document as $key=>$field) + $var[$key]=$field; + } + + /** + * Return field names + * @return array + **/ + function fields() { + return array_keys($this->document); + } + + /** + * Return the cursor from last query + * @return object|NULL + **/ + function cursor() { + return $this->cursor; + } + + /** + * Instantiate class + * @return void + * @param $db object + * @param $collection string + **/ + function __construct(\DB\Mongo $db,$collection) { + $this->db=$db; + $this->collection=$db->{$collection}; + $this->reset(); + } + +} diff --git a/management-interface/lib/db/mongo/session.php b/management-interface/lib/db/mongo/session.php new file mode 100644 index 0000000..e3c6665 --- /dev/null +++ b/management-interface/lib/db/mongo/session.php @@ -0,0 +1,174 @@ +sid) + $this->load(array('session_id'=>$this->sid=$id)); + return $this->dry()?FALSE:$this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write($id,$data) { + $fw=\Base::instance(); + $sent=headers_sent(); + $headers=$fw->get('HEADERS'); + if ($id!=$this->sid) + $this->load(array('session_id'=>$this->sid=$id)); + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('csrf',$sent?$this->csrf():$csrf); + $this->set('ip',$fw->get('IP')); + $this->set('agent', + isset($headers['User-Agent'])?$headers['User-Agent']:''); + $this->set('stamp',time()); + $this->save(); + if (!$sent) { + if (isset($_COOKIE['_'])) + setcookie('_','',strtotime('-1 year')); + call_user_func_array('setcookie', + array('_',$csrf)+$fw->get('JAR')); + } + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id) { + $this->erase(array('session_id'=>$id)); + setcookie(session_name(),'',strtotime('-1 year')); + unset($_COOKIE[session_name()]); + header_remove('Set-Cookie'); + return TRUE; + } + + /** + * Garbage collector + * @return TRUE + * @param $max int + **/ + function cleanup($max) { + $this->erase(array('$where'=>'this.stamp+'.$max.'<'.time())); + return TRUE; + } + + /** + * Return anti-CSRF token + * @return string|FALSE + **/ + function csrf() { + return $this->dry()?FALSE:$this->get('csrf'); + } + + /** + * Return IP address + * @return string|FALSE + **/ + function ip() { + return $this->dry()?FALSE:$this->get('ip'); + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string|FALSE + **/ + function agent() { + return $this->dry()?FALSE:$this->get('agent'); + } + + /** + * Instantiate class + * @param $db object + * @param $table string + **/ + function __construct(\DB\Mongo $db,$table='sessions') { + parent::__construct($db,$table); + session_set_save_handler( + array($this,'open'), + array($this,'close'), + array($this,'read'), + array($this,'write'), + array($this,'destroy'), + array($this,'cleanup') + ); + register_shutdown_function('session_commit'); + @session_start(); + $fw=\Base::instance(); + $headers=$fw->get('HEADERS'); + if (($ip=$this->ip()) && $ip!=$fw->get('IP') || + ($agent=$this->agent()) && + (!isset($headers['User-Agent']) || + $agent!=$headers['User-Agent'])) { + session_destroy(); + $fw->error(403); + } + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + if ($this->load(array('session_id'=>$this->sid=session_id()))) { + $this->set('csrf',$csrf); + $this->save(); + } + } + +} diff --git a/management-interface/lib/db/sql.php b/management-interface/lib/db/sql.php new file mode 100644 index 0000000..88e34dc --- /dev/null +++ b/management-interface/lib/db/sql.php @@ -0,0 +1,403 @@ +trans=TRUE; + return $out; + } + + /** + * Rollback SQL transaction + * @return bool + **/ + function rollback() { + $out=parent::rollback(); + $this->trans=FALSE; + return $out; + } + + /** + * Commit SQL transaction + * @return bool + **/ + function commit() { + $out=parent::commit(); + $this->trans=FALSE; + return $out; + } + + /** + * Map data type of argument to a PDO constant + * @return int + * @param $val scalar + **/ + function type($val) { + switch (gettype($val)) { + case 'NULL': + return \PDO::PARAM_NULL; + case 'boolean': + return \PDO::PARAM_BOOL; + case 'integer': + return \PDO::PARAM_INT; + default: + return \PDO::PARAM_STR; + } + } + + /** + * Cast value to PHP type + * @return scalar + * @param $type string + * @param $val scalar + **/ + function value($type,$val) { + switch ($type) { + case \PDO::PARAM_NULL: + return (unset)$val; + case \PDO::PARAM_INT: + return (int)$val; + case \PDO::PARAM_BOOL: + return (bool)$val; + case \PDO::PARAM_STR: + return (string)$val; + } + } + + /** + * Execute SQL statement(s) + * @return array|int|FALSE + * @param $cmds string|array + * @param $args string|array + * @param $ttl int + * @param $log bool + **/ + function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { + $auto=FALSE; + if (is_null($args)) + $args=array(); + elseif (is_scalar($args)) + $args=array(1=>$args); + if (is_array($cmds)) { + if (count($args)<($count=count($cmds))) + // Apply arguments to SQL commands + $args=array_fill(0,$count,$args); + if (!$this->trans) { + $this->begin(); + $auto=TRUE; + } + } + else { + $cmds=array($cmds); + $args=array($args); + } + $fw=\Base::instance(); + $cache=\Cache::instance(); + $result=FALSE; + foreach (array_combine($cmds,$args) as $cmd=>$arg) { + if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) + continue; + $now=microtime(TRUE); + $keys=$vals=array(); + if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists( + $hash=$fw->hash($this->dsn.$cmd. + $fw->stringify($arg)).'.sql',$result)) && + $cached[0]+$ttl>microtime(TRUE)) { + foreach ($arg as $key=>$val) { + $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + } + elseif (is_object($query=$this->prepare($cmd))) { + foreach ($arg as $key=>$val) { + if (is_array($val)) { + // User-specified data type + $query->bindvalue($key,$val[0],$val[1]); + $vals[]=$fw->stringify($this->value($val[1],$val[0])); + } + else { + // Convert to PDO data type + $query->bindvalue($key,$val, + $type=$this->type($val)); + $vals[]=$fw->stringify($this->value($type,$val)); + } + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + $query->execute(); + $error=$query->errorinfo(); + if ($error[0]!=\PDO::ERR_NONE) { + // Statement-level error occurred + if ($this->trans) + $this->rollback(); + user_error('PDOStatement: '.$error[2]); + } + if (preg_match('/^\s*'. + '(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING|EXEC)\b/is', + $cmd)) { + $result=$query->fetchall(\PDO::FETCH_ASSOC); + // Work around SQLite quote bug + if (preg_match('/sqlite2?/',$this->engine)) + foreach ($result as $pos=>$rec) { + unset($result[$pos]); + $result[$pos]=array(); + foreach ($rec as $key=>$val) + $result[$pos][trim($key,'\'"[]`')]=$val; + } + $this->rows=count($result); + if ($fw->get('CACHE') && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + else + $this->rows=$result=$query->rowcount(); + $query->closecursor(); + unset($query); + } + else { + $error=$this->errorinfo(); + if ($error[0]!=\PDO::ERR_NONE) { + // PDO-level error occurred + if ($this->trans) + $this->rollback(); + user_error('PDO: '.$error[2]); + } + } + if ($log) + $this->log.=date('r').' ('. + sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + (empty($cached)?'':'[CACHED] '). + preg_replace($keys,$vals,$cmd,1).PHP_EOL; + } + if ($this->trans && $auto) + $this->commit(); + return $result; + } + + /** + * Return number of rows affected by last query + * @return int + **/ + function count() { + return $this->rows; + } + + /** + * Return SQL profiler results + * @return string + **/ + function log() { + return $this->log; + } + + /** + * Retrieve schema of SQL table + * @return array|FALSE + * @param $table string + * @param $fields array|string + * @param $ttl int + **/ + function schema($table,$fields=NULL,$ttl=0) { + // Supported engines + $cmd=array( + 'sqlite2?'=>array( + 'PRAGMA table_info("'.$table.'");', + 'name','type','dflt_value','notnull',0,'pk',TRUE), + 'mysql'=>array( + 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;', + 'Field','Type','Default','Null','YES','Key','PRI'), + 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array( + 'SELECT '. + 'c.column_name AS field,'. + 'c.data_type AS type,'. + 'c.column_default AS defval,'. + 'c.is_nullable AS nullable,'. + 't.constraint_type AS pkey '. + 'FROM information_schema.columns AS c '. + 'LEFT OUTER JOIN '. + 'information_schema.key_column_usage AS k '. + 'ON '. + 'c.table_name=k.table_name AND '. + 'c.column_name=k.column_name AND '. + 'c.table_schema=k.table_schema '. + ($this->dbname? + ('AND c.table_catalog=k.table_catalog '):''). + 'LEFT OUTER JOIN '. + 'information_schema.table_constraints AS t ON '. + 'k.table_name=t.table_name AND '. + 'k.constraint_name=t.constraint_name '. + 'k.table_schema=t.table_schema '. + ($this->dbname? + ('AND k.table_catalog=t.table_catalog '):''). + 'WHERE '. + 'c.table_name='.$this->quote($table).' '. + ($this->dbname? + ('AND c.table_catalog='. + $this->quote($this->dbname)):''). + ';', + 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'), + 'oci'=>array( + 'SELECT c.column_name AS field, '. + 'c.data_type AS type, '. + 'c.data_default AS defval, '. + 'c.nullable AS nullable, '. + '(SELECT t.constraint_type '. + 'FROM all_cons_columns acc '. + 'LEFT OUTER JOIN all_constraints t '. + 'ON acc.constraint_name=t.constraint_name '. + 'WHERE acc.table_name='.$this->quote($table).' '. + 'AND acc.column_name=c.column_name '. + 'AND constraint_type='.$this->quote('P').') AS pkey '. + 'FROM all_tab_cols c '. + 'WHERE c.table_name='.$this->quote($table), + 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P') + ); + if (is_string($fields)) + $fields=\Base::instance()->split($fields); + foreach ($cmd as $key=>$val) + if (preg_match('/'.$key.'/',$this->engine)) { + // Improve InnoDB performance on MySQL with + // SET GLOBAL innodb_stats_on_metadata=0; + // This requires SUPER privilege! + $rows=array(); + foreach ($this->exec($val[0],NULL,$ttl) as $row) { + if (!$fields || in_array($row[$val[1]],$fields)) + $rows[$row[$val[1]]]=array( + 'type'=>$row[$val[2]], + 'pdo_type'=> + preg_match('/int\b|int(?=eger)|bool/i', + $row[$val[2]],$parts)? + constant('\PDO::PARAM_'. + strtoupper($parts[0])): + \PDO::PARAM_STR, + 'default'=>$row[$val[3]], + 'nullable'=>$row[$val[4]]==$val[5], + 'pkey'=>$row[$val[6]]==$val[7] + ); + } + return $rows; + } + return FALSE; + } + + /** + * Quote string + * @return string + * @param $val mixed + * @param $type int + **/ + function quote($val,$type=\PDO::PARAM_STR) { + return $this->engine=='odbc'? + (is_string($val)? + \Base::instance()->stringify(str_replace('\'','\'\'',$val)): + $val): + parent::quote($val,$type); + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return database engine + * @return string + **/ + function driver() { + return $this->engine; + } + + /** + * Return server version + * @return string + **/ + function version() { + return parent::getattribute(parent::ATTR_SERVER_VERSION); + } + + /** + * Return database name + * @return string + **/ + function name() { + return $this->dbname; + } + + /** + * Return quoted identifier name + * @return string + * @param $key + **/ + function quotekey($key) { + if ($this->engine=='mysql') + $key="`".implode('`.`',explode('.',$key))."`"; + elseif (preg_match('/sybase|dblib/',$this->engine)) + $key="'".implode("'.'",explode('.',$key))."'"; + elseif (preg_match('/sqlite2?|pgsql|oci/',$this->engine)) + $key='"'.implode('"."',explode('.',$key)).'"'; + elseif (preg_match('/mssql|sqlsrv|odbc/',$this->engine)) + $key="[".implode('].[',explode('.',$key))."]"; + return $key; + } + + /** + * Instantiate class + * @param $dsn string + * @param $user string + * @param $pw string + * @param $options array + **/ + function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) { + $fw=\Base::instance(); + $this->uuid=$fw->hash($this->dsn=$dsn); + if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/i',$dsn,$parts)) + $this->dbname=$parts[1]; + if (!$options) + $options=array(); + if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') + $options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. + strtolower(str_replace('-','',$fw->get('ENCODING'))).';'); + parent::__construct($dsn,$user,$pw,$options); + $this->engine=parent::getattribute(parent::ATTR_DRIVER_NAME); + } + +} diff --git a/management-interface/lib/db/sql/mapper.php b/management-interface/lib/db/sql/mapper.php new file mode 100644 index 0000000..6af4675 --- /dev/null +++ b/management-interface/lib/db/sql/mapper.php @@ -0,0 +1,552 @@ +fields+$this->adhoc); + } + + /** + * Assign value to field + * @return scalar + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + if (array_key_exists($key,$this->fields)) { + $val=is_null($val) && $this->fields[$key]['nullable']? + NULL:$this->db->value($this->fields[$key]['pdo_type'],$val); + if ($this->fields[$key]['value']!==$val || + $this->fields[$key]['default']!==$val && is_null($val)) + $this->fields[$key]['changed']=TRUE; + return $this->fields[$key]['value']=$val; + } + // Parenthesize expression in case it's a subquery + $this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL); + return $val; + } + + /** + * Retrieve value of field + * @return scalar + * @param $key string + **/ + function get($key) { + if ($key=='_id') + return $this->_id; + elseif (array_key_exists($key,$this->fields)) + return $this->fields[$key]['value']; + elseif (array_key_exists($key,$this->adhoc)) + return $this->adhoc[$key]['value']; + user_error(sprintf(self::E_Field,$key)); + } + + /** + * Clear value of field + * @return NULL + * @param $key string + **/ + function clear($key) { + if (array_key_exists($key,$this->adhoc)) + unset($this->adhoc[$key]); + } + + /** + * Get PHP type equivalent of PDO constant + * @return string + * @param $pdo string + **/ + function type($pdo) { + switch ($pdo) { + case \PDO::PARAM_NULL: + return 'unset'; + case \PDO::PARAM_INT: + return 'int'; + case \PDO::PARAM_BOOL: + return 'bool'; + case \PDO::PARAM_STR: + return 'string'; + } + } + + /** + * Convert array to mapper object + * @return object + * @param $row array + **/ + protected function factory($row) { + $mapper=clone($this); + $mapper->reset(); + foreach ($row as $key=>$val) { + if (array_key_exists($key,$this->fields)) + $var='fields'; + elseif (array_key_exists($key,$this->adhoc)) + $var='adhoc'; + else + continue; + $mapper->{$var}[$key]['value']=$val; + if ($var=='fields' && $mapper->{$var}[$key]['pkey']) + $mapper->{$var}[$key]['previous']=$val; + } + $mapper->query=array(clone($mapper)); + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return array_map( + function($row) { + return $row['value']; + }, + $obj->fields+$obj->adhoc + ); + } + + /** + * Build query string and execute + * @return array + * @param $fields string + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { + if (!$options) + $options=array(); + $options+=array( + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ); + $sql='SELECT '.$fields.' FROM '.$this->table; + $args=array(); + if ($filter) { + if (is_array($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:array(1=>$args); + list($filter)=$filter; + } + $sql.=' WHERE '.$filter; + } + $db=$this->db; + if ($options['group']) + $sql.=' GROUP BY '.implode(',',array_map( + function($str) use($db) { + return preg_match('/^(\w+)(?:\h+HAVING|\h*(?:,|$))/i', + $str,$parts)? + ($db->quotekey($parts[1]). + (isset($parts[2])?(' '.$parts[2]):'')):$str; + }, + explode(',',$options['group']))); + if ($options['order']) { + $sql.=' ORDER BY '.implode(',',array_map( + function($str) use($db) { + return preg_match('/^(\w+)(?:\h+(ASC|DESC))?\h*(?:,|$)/i', + $str,$parts)? + ($db->quotekey($parts[1]). + (isset($parts[2])?(' '.$parts[2]):'')):$str; + }, + explode(',',$options['order']))); + } + if ($options['limit']) + $sql.=' LIMIT '.(int)$options['limit']; + if ($options['offset']) + $sql.=' OFFSET '.(int)$options['offset']; + $result=$this->db->exec($sql,$args,$ttl); + $out=array(); + foreach ($result as &$row) { + foreach ($row as $field=>&$val) { + if (array_key_exists($field,$this->fields)) { + if (!is_null($val) || !$this->fields[$field]['nullable']) + $val=$this->db->value( + $this->fields[$field]['pdo_type'],$val); + } + elseif (array_key_exists($field,$this->adhoc)) + $this->adhoc[$field]['value']=$val; + unset($val); + } + $out[]=$this->factory($row); + unset($row); + } + return $out; + } + + /** + * Return records that match criteria + * @return array + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function find($filter=NULL,array $options=NULL,$ttl=0) { + if (!$options) + $options=array(); + $options+=array( + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ); + $adhoc=''; + foreach ($this->adhoc as $key=>$field) + $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); + return $this->select(implode(',', + array_map(array($this->db,'quotekey'),array_keys($this->fields))). + $adhoc,$filter,$options,$ttl); + } + + /** + * Count records that match criteria + * @return int + * @param $filter string|array + * @param $ttl int + **/ + function count($filter=NULL,$ttl=0) { + $sql='SELECT COUNT(*) AS '. + $this->db->quotekey('rows').' FROM '.$this->table; + $args=array(); + if ($filter) { + if (is_array($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:array(1=>$args); + list($filter)=$filter; + } + $sql.=' WHERE '.$filter; + } + $result=$this->db->exec($sql,$args,$ttl); + return $result[0]['rows']; + } + + /** + * Return record at specified offset using same criteria as + * previous load() call and make it active + * @return array + * @param $ofs int + **/ + function skip($ofs=1) { + $out=parent::skip($ofs); + $dry=$this->dry(); + foreach ($this->fields as $key=>&$field) { + $field['value']=$dry?NULL:$out->fields[$key]['value']; + $field['changed']=FALSE; + if ($field['pkey']) + $field['previous']=$dry?NULL:$out->fields[$key]['value']; + unset($field); + } + foreach ($this->adhoc as $key=>&$field) { + $field['value']=$dry?NULL:$out->adhoc[$key]['value']; + unset($field); + } + if (isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return object + **/ + function insert() { + $args=array(); + $ctr=0; + $fields=''; + $values=''; + $filter=''; + $pkeys=array(); + $nkeys=array(); + $ckeys=array(); + $inc=NULL; + foreach ($this->fields as $key=>$field) + if ($field['pkey']) + $pkeys[$key]=$field['previous']; + if (isset($this->trigger['beforeinsert'])) + \Base::instance()->call($this->trigger['beforeinsert'], + array($this,$pkeys)); + foreach ($this->fields as $key=>&$field) { + if ($field['pkey']) { + $field['previous']=$field['value']; + if (!$inc && $field['pdo_type']==\PDO::PARAM_INT && + empty($field['value']) && !$field['nullable']) + $inc=$key; + $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; + $nkeys[$ctr+1]=array($field['value'],$field['pdo_type']); + } + if ($field['changed'] && $key!=$inc) { + $fields.=($ctr?',':'').$this->db->quotekey($key); + $values.=($ctr?',':'').'?'; + $args[$ctr+1]=array($field['value'],$field['pdo_type']); + $ctr++; + $ckeys[]=$key; + } + $field['changed']=FALSE; + unset($field); + } + if ($fields) { + $this->db->exec( + (preg_match('/mssql|dblib|sqlsrv/',$this->engine) && + array_intersect(array_keys($pkeys),$ckeys)? + 'SET IDENTITY_INSERT '.$this->table.' ON;':''). + 'INSERT INTO '.$this->table.' ('.$fields.') '. + 'VALUES ('.$values.')',$args + ); + $seq=NULL; + if ($this->engine=='pgsql') { + $names=array_keys($pkeys); + $seq=$this->source.'_'.end($names).'_seq'; + } + if ($this->engine!='oci') + $this->_id=$this->db->lastinsertid($seq); + // Reload to obtain default and auto-increment field values + $this->load($inc? + array($inc.'=?',$this->db->value( + $this->fields[$inc]['pdo_type'],$this->_id)): + array($filter,$nkeys)); + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + array($this,$pkeys)); + } + return $this; + } + + /** + * Update current record + * @return object + **/ + function update() { + $args=array(); + $ctr=0; + $pairs=''; + $filter=''; + $pkeys=array(); + foreach ($this->fields as $key=>$field) + if ($field['pkey']) + $pkeys[$key]=$field['previous']; + if (isset($this->trigger['beforeupdate'])) + \Base::instance()->call($this->trigger['beforeupdate'], + array($this,$pkeys)); + foreach ($this->fields as $key=>$field) + if ($field['changed']) { + $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; + $args[$ctr+1]=array($field['value'],$field['pdo_type']); + $ctr++; + } + foreach ($this->fields as $key=>$field) + if ($field['pkey']) { + $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; + $args[$ctr+1]=array($field['previous'],$field['pdo_type']); + $ctr++; + } + if ($pairs) { + $sql='UPDATE '.$this->table.' SET '.$pairs; + if ($filter) + $sql.=' WHERE '.$filter; + $this->db->exec($sql,$args); + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + array($this,$pkeys)); + } + return $this; + } + + /** + * Delete current record + * @return int + * @param $filter string|array + **/ + function erase($filter=NULL) { + if ($filter) { + $args=array(); + if (is_array($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:array(1=>$args); + list($filter)=$filter; + } + return $this->db-> + exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); + } + $args=array(); + $ctr=0; + $filter=''; + $pkeys=array(); + foreach ($this->fields as $key=>&$field) { + if ($field['pkey']) { + $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; + $args[$ctr+1]=array($field['previous'],$field['pdo_type']); + $pkeys[$key]=$field['previous']; + $ctr++; + } + $field['value']=NULL; + $field['changed']=(bool)$field['default']; + if ($field['pkey']) + $field['previous']=NULL; + unset($field); + } + foreach ($this->adhoc as &$field) { + $field['value']=NULL; + unset($field); + } + parent::erase(); + $this->skip(0); + if (isset($this->trigger['beforeerase'])) + \Base::instance()->call($this->trigger['beforeerase'], + array($this,$pkeys)); + $out=$this->db-> + exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); + if (isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + array($this,$pkeys)); + return $out; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + foreach ($this->fields as &$field) { + $field['value']=NULL; + $field['changed']=FALSE; + if ($field['pkey']) + $field['previous']=NULL; + unset($field); + } + foreach ($this->adhoc as &$field) { + $field['value']=NULL; + unset($field); + } + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $key string + * @param $func callback + **/ + function copyfrom($key,$func=NULL) { + $var=\Base::instance()->get($key); + if ($func) + $var=$func($var); + foreach ($var as $key=>$val) + if (in_array($key,array_keys($this->fields))) { + $field=&$this->fields[$key]; + if ($field['value']!==$val) { + $field['value']=$val; + $field['changed']=TRUE; + } + unset($field); + } + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->fields+$this->adhoc as $key=>$field) + $var[$key]=$field['value']; + } + + /** + * Return schema + * @return array + **/ + function schema() { + return $this->fields; + } + + /** + * Return field names + * @return array + * @param $adhoc bool + **/ + function fields($adhoc=TRUE) { + return array_keys($this->fields+($adhoc?$this->adhoc:array())); + } + + /** + * Instantiate class + * @param $db object + * @param $table string + * @param $fields array|string + * @param $ttl int + **/ + function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) { + $this->db=$db; + $this->engine=$db->driver(); + if ($this->engine=='oci') + $table=strtoupper($table); + $this->source=$table; + $this->table=$this->db->quotekey($table); + $this->fields=$db->schema($table,$fields,$ttl); + $this->reset(); + } + +} diff --git a/management-interface/lib/db/sql/session.php b/management-interface/lib/db/sql/session.php new file mode 100644 index 0000000..48050ec --- /dev/null +++ b/management-interface/lib/db/sql/session.php @@ -0,0 +1,187 @@ +sid) + $this->load(array('session_id=?',$this->sid=$id)); + return $this->dry()?FALSE:$this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write($id,$data) { + $fw=\Base::instance(); + $sent=headers_sent(); + $headers=$fw->get('HEADERS'); + if ($id!=$this->sid) + $this->load(array('session_id=?',$this->sid=$id)); + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('csrf',$sent?$this->csrf():$csrf); + $this->set('ip',$fw->get('IP')); + $this->set('agent', + isset($headers['User-Agent'])?$headers['User-Agent']:''); + $this->set('stamp',time()); + $this->save(); + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id) { + $this->erase(array('session_id=?',$id)); + setcookie(session_name(),'',strtotime('-1 year')); + unset($_COOKIE[session_name()]); + header_remove('Set-Cookie'); + return TRUE; + } + + /** + * Garbage collector + * @return TRUE + * @param $max int + **/ + function cleanup($max) { + $this->erase(array('stamp+?dry()?FALSE:$this->get('csrf'); + } + + /** + * Return IP address + * @return string|FALSE + **/ + function ip() { + return $this->dry()?FALSE:$this->get('ip'); + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string|FALSE + **/ + function agent() { + return $this->dry()?FALSE:$this->get('agent'); + } + + /** + * Instantiate class + * @param $db object + * @param $table string + * @param $force bool + **/ + function __construct(\DB\SQL $db,$table='sessions',$force=TRUE) { + if ($force) + $db->exec( + (preg_match('/mssql|sqlsrv|sybase/',$db->driver())? + ('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '. + 'name='.$db->quote($table).' AND xtype=\'U\') '. + 'CREATE TABLE dbo.'): + ('CREATE TABLE IF NOT EXISTS '. + (($name=$db->name())?($name.'.'):''))). + $table.' ('. + 'session_id VARCHAR(40),'. + 'data TEXT,'. + 'csrf TEXT,'. + 'ip VARCHAR(40),'. + 'agent VARCHAR(255),'. + 'stamp INTEGER,'. + 'PRIMARY KEY(session_id)'. + ');' + ); + parent::__construct($db,$table); + session_set_save_handler( + array($this,'open'), + array($this,'close'), + array($this,'read'), + array($this,'write'), + array($this,'destroy'), + array($this,'cleanup') + ); + register_shutdown_function('session_commit'); + @session_start(); + $fw=\Base::instance(); + $headers=$fw->get('HEADERS'); + if (($ip=$this->ip()) && $ip!=$fw->get('IP') || + ($agent=$this->agent()) && + (!isset($headers['User-Agent']) || + $agent!=$headers['User-Agent'])) { + session_destroy(); + $fw->error(403); + } + $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->hash(mt_rand()); + if ($this->load(array('session_id=?',$this->sid=session_id()))) { + $this->set('csrf',$csrf); + $this->save(); + } + } + +} -- cgit v1.2.3-55-g7522