From 943df693fa3b183f7fec6fb09f9c9f89413228e6 Mon Sep 17 00:00:00 2001 From: ikkez Date: Thu, 31 Dec 2015 18:20:37 +0100 Subject: [PATCH] 3.5.1-Release --- lib/CHANGELOG | 47 ++++++++++++ lib/base.php | 162 ++++++++++++++++++++++----------------- lib/basket.php | 2 +- lib/db/cursor.php | 7 +- lib/db/jig/mapper.php | 4 +- lib/db/jig/session.php | 101 +++++++++++++----------- lib/db/mongo/mapper.php | 8 +- lib/db/mongo/session.php | 113 ++++++++++++++------------- lib/db/sql.php | 29 +++++-- lib/db/sql/mapper.php | 16 ++-- lib/db/sql/session.php | 96 ++++++++++++----------- lib/image.php | 16 +++- lib/session.php | 124 +++++++++++++++--------------- lib/smtp.php | 39 +++++----- lib/template.php | 63 +++++++-------- lib/web.php | 24 ++++-- 16 files changed, 483 insertions(+), 368 deletions(-) diff --git a/lib/CHANGELOG b/lib/CHANGELOG index d7179d106..cf2dab512 100644 --- a/lib/CHANGELOG +++ b/lib/CHANGELOG @@ -1,5 +1,52 @@ CHANGELOG +3.5.1 (31 December 2015) +* NEW: ttl attribute in template tag +* NEW: allow anonymous function for template filter +* NEW: format modifier for international and custom currency symbol +* NEW: Image->data() returns image resource +* NEW: extract() get prefixed array keys from an assoc array +* NEW: Optimized and faster Template parser with full support for HTML5 empty tags +* NEW: Added support for {@token} encapsulation syntax in routes definition +* NEW: DB\SQL->exec(), automatically shift to 1-based query arguments +* NEW: abort() flush output +* Added referenced value to devoid() +* Template token filters are now resolved within Preview->token() +* Web->_curl: restrict redirections to HTTP +* Web->minify(), skip importing of external files +* Improved session and error handling in until() +* Get the error trace array with the new $format parameter +* Better support for unicode URLs +* Optimized TZ detection with date_default_timezone_get() +* format() Provide default decimal places +* Optimize code: remove redundant TTL checks +* Optimized timeout handling in Web->request() +* Improved PHPDoc hints +* Added missing russian DIACRITICS letters +* DB\Cursor: allow child implementation of reset() +* DB\Cursor: Copyfrom now does an internal call to set() +* DB\SQL: Provide the ability to disable SQL logging +* DB\SQL: improved query analysis to trigger fetchAll +* DB\SQL\Mapper: added support for binary table columns +* SQL,JIG,MONGO,CACHE Session handlers refactored and optimized +* SMTP Refactoring and optimization +* Bug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing) +* Bug fix: SMTP, Send buffered optional headers to output +* Bug fix: SMTP, Content-Transfer-Encoding for non-TLS connections +* Bug fix: SMTP, Single attachment error +* Bug fix: Cursor->load not always mapping to first record +* Bug fix: dry SQL mapper should not trigger 'load' +* Bug fix: Code highlighting on empty text +* Bug fix: Image->resize, round dimensions instead of cast +* Bug fix: whitespace handling in $f3->compile() +* Bug fix: TTL of `View` and `Preview` (`Template`) +* Bug fix: token filter regex +* Bug fix: Template, empty attributes +* Bug fix: Preview->build() greedy regex +* Bug fix: Web->minify() single-line comment on last line +* Bug fix: Web->request(), follow_location with cURL and open_basedir +* Bug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers + 3.5.0 (2 June 2015) * NEW: until() method for long polling * NEW: abort() to disconnect HTTP client (and continue execution) diff --git a/lib/base.php b/lib/base.php index 0855f9d1e..ea1a1f09a 100644 --- a/lib/base.php +++ b/lib/base.php @@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess { //@{ Framework details const PACKAGE='Fat-Free Framework', - VERSION='3.5.0-Release'; + VERSION='3.5.1-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -179,8 +179,8 @@ function($match) use(&$i,$params) { } /** - * assemble url from alias name - * @return NULL + * Assemble url from alias name + * @return string * @param $name string * @param $params array|string **/ @@ -215,7 +215,7 @@ function parse($str) { function compile($str) { $fw=$this; return preg_replace_callback( - '/(?|::)*)/', + '/(?|::)*)/', function($var) use($fw) { return '$'.preg_replace_callback( '/\.(\w+)\(|\.(\w+)|\[((?:[^\[\]]*|(?R))*)\]/', @@ -226,7 +226,7 @@ function($expr) use($fw) { ('['.var_export($expr[1],TRUE).']')).'('): ('['.var_export( isset($expr[3])? - $fw->compile($expr[3]): + trim($fw->compile($expr[3])): (ctype_digit($expr[2])? (int)$expr[2]: $expr[2]),TRUE).']'); @@ -305,10 +305,11 @@ function exists($key,&$val=NULL) { /** * Return TRUE if hive key is empty and not cached - * @return bool * @param $key string + * @param $val mixed + * @return bool **/ - function devoid($key) { + function devoid($key,&$val=NULL) { $val=$this->ref($key,FALSE); return empty($val) && (!Cache::instance()->exists($this->hash($key).'.var',$val) || @@ -422,8 +423,7 @@ function clear($key) { // End session session_unset(); session_destroy(); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); + $this->clear('COOKIE.'.session_name()); } $this->sync('SESSION'); } @@ -679,6 +679,19 @@ function sign($num) { return $num?($num/abs($num)):0; } + /** + * Extract values of an associative array whose keys start with the given prefix + * @return array + * @param $arr array + * @param $prefix string + **/ + function extract($arr,$prefix) { + $out=array(); + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) as $key) + $out[substr($key,strlen($prefix))]=$arr[$key]; + return $out; + } + /** * Convert class constants to array * @return array @@ -687,14 +700,7 @@ function sign($num) { **/ function constants($class,$prefix='') { $ref=new ReflectionClass($class); - $out=array(); - foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants())) - as $val) { - $out[$key=substr($val,strlen($prefix))]= - constant((is_object($class)?get_class($class):$class).'::'.$prefix.$key); - } - unset($ref); - return $out; + return $this->extract($ref->getconstants(),$prefix); } /** @@ -843,9 +849,12 @@ function($expr) use($args,$conv) { return number_format( $args[$pos],0,'',$thousands_sep); case 'currency': - if (function_exists('money_format')) + $int=$cstm=false; + if (isset($prop) && $cstm=!$int=($prop=='int')) + $currency_symbol=$prop; + if (!$cstm && function_exists('money_format')) return money_format( - '%n',$args[$pos]); + '%'.($int?'i':'n'),$args[$pos]); $fmt=array( 0=>'(nc)',1=>'(n c)', 2=>'(nc)',10=>'+nc', @@ -878,7 +887,8 @@ function($expr) use($args,$conv) { $frac_digits, $decimal_point, $thousands_sep), - $currency_symbol), + $int?$int_curr_symbol + :$currency_symbol), $fmt[(int)( (${$pre.'_cs_precedes'}%2). (${$pre.'_sign_posn'}%5). @@ -891,8 +901,8 @@ function($expr) use($args,$conv) { $thousands_sep).'%'; case 'decimal': return number_format( - $args[$pos],$prop,$decimal_point, - $thousands_sep); + $args[$pos],isset($prop)?$prop:2, + $decimal_point,$thousands_sep); } break; case 'date': @@ -1022,7 +1032,7 @@ function unserialize($arg) { **/ function status($code) { $reason=@constant('self::HTTP_'.$code); - if (PHP_SAPI!='cli') + if (PHP_SAPI!='cli' && !headers_sent()) header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); return $reason; } @@ -1089,11 +1099,12 @@ function ip() { } /** - * Return formatted stack trace - * @return string + * Return filtered, formatted stack trace + * @return string|array * @param $trace array|NULL + * @param $format bool **/ - function trace(array $trace=NULL) { + function trace(array $trace=NULL, $format=TRUE) { if (!$trace) { $trace=debug_backtrace(FALSE); $frame=$trace[0]; @@ -1111,6 +1122,8 @@ function($frame) use($debug) { '__call|call_user_func)/',$frame['function'])); } ); + if (!$format) + return $trace; $out=''; $eol="\n"; // Analyze stack trace @@ -1369,7 +1382,7 @@ function mask($pattern,$url=NULL) { $url=$this->rel($this->hive['URI']); $case=$this->hive['CASELESS']?'i':''; preg_match('/^'. - preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)', + preg_replace('/((\\\{)?@(\w+\b)(?(2)\\\}))/','(?P<\3>[^\/\?]+)', str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). '\/?(?:\?.*)?$/'.$case.'um',$url,$args); return $args; @@ -1394,7 +1407,7 @@ function run() { array_multisort($paths,SORT_DESC,$keys,$vals); $this->hive['ROUTES']=array_combine($keys,$vals); // Convert to BASE-relative URL - $req=$this->rel($this->hive['URI']); + $req=$this->rel(urldecode($this->hive['URI'])); if ($cors=(isset($this->hive['HEADERS']['Origin']) && $this->hive['CORS']['origin'])) { $cors=$this->hive['CORS']; @@ -1428,7 +1441,7 @@ function run() { if (is_numeric($key) && $key) unset($args[$key]); // Capture values of route pattern tokens - $this->hive['PARAMS']=$args=array_map('urldecode',$args); + $this->hive['PARAMS']=$args; // Save matching route $this->hive['ALIAS']=$alias; $this->hive['PATTERN']=$pattern; @@ -1437,9 +1450,10 @@ function run() { implode(',',$cors['expose']):$cors['expose'])); if (is_string($handler)) { // Replace route pattern tokens in handler if any - $handler=preg_replace_callback('/@(\w+\b)/', + $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', function($id) use($args) { - return isset($args[$id[1]])?$args[$id[1]]:$id[0]; + $pid=count($id)>2?2:1; + return isset($args[$id[$pid]])?$args[$id[$pid]]:$id[0]; }, $handler ); @@ -1458,7 +1472,7 @@ function($id) use($args) { $cached=$cache->exists( $hash=$this->hash($this->hive['VERB'].' '. $this->hive['URI']).'.url',$data); - if ($cached && $cached[0]+$ttl>$now) { + if ($cached) { if (isset($headers['If-Modified-Since']) && strtotime($headers['If-Modified-Since'])+ $ttl>$now) { @@ -1520,10 +1534,13 @@ function($id) use($args) { // Unhandled HTTP method header('Allow: '.implode(',',array_unique($allowed))); if ($cors) { - header('Access-Control-Allow-Methods: OPTIONS,'.implode(',',$allowed)); + header('Access-Control-Allow-Methods: OPTIONS,'. + implode(',',$allowed)); if ($cors['headers']) - header('Access-Control-Allow-Headers: '.(is_array($cors['headers'])? - implode(',',$cors['headers']):$cors['headers'])); + header('Access-Control-Allow-Headers: '. + (is_array($cors['headers'])? + implode(',',$cors['headers']): + $cors['headers'])); if ($cors['ttl']>0) header('Access-Control-Max-Age: '.$cors['ttl']); } @@ -1546,28 +1563,26 @@ function until($func,$args=NULL,$timeout=60) { $time=time(); $limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1)); $out=''; - $flag=FALSE; + // Turn output buffering on + ob_start(); // Not for the weak of heart while ( + // No error occurred + !$this->hive['ERROR'] && // Still alive? !connection_aborted() && // Got time left? (time()-$time+1<$limit) && // Restart session - $flag=@session_start() && + @session_start() && // CAUTION: Callback will kill host if it never becomes truthy! !($out=$this->call($func,$args))) { session_commit(); - ob_flush(); - flush(); // Hush down sleep(1); } - if ($flag) { - session_commit(); - ob_flush(); - flush(); - } + ob_flush(); + flush(); return $out; } @@ -1577,9 +1592,11 @@ function until($func,$args=NULL,$timeout=60) { function abort() { @session_start(); session_commit(); - header('Content-Length: 0'); + $out=''; while (ob_get_level()) - ob_end_clean(); + $out=ob_get_clean().$out; + header('Content-Length: '.strlen($out)); + echo $out; flush(); if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); @@ -1826,7 +1843,7 @@ function highlight($text) { $out=''; $pre=FALSE; $text=trim($text); - if (!preg_match('/^<\?php/',$text)) { + if ($text && !preg_match('/^<\?php/',$text)) { $text='NULL, 'CORS'=>array( 'headers'=>'', - 'origin'=>false, - 'credentials'=>false, - 'expose'=>false, + 'origin'=>FALSE, + 'credentials'=>FALSE, + 'expose'=>FALSE, 'ttl'=>0), 'DEBUG'=>0, 'DIACRITICS'=>array(), @@ -2131,7 +2148,7 @@ function($code,$text) use($fw) { 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', 'TEMP'=>'tmp/', 'TIME'=>microtime(TRUE), - 'TZ'=>(@ini_get('date.timezone'))?:'UTC', + 'TZ'=>@date_default_timezone_get(), 'UI'=>'./', 'UNLOAD'=>NULL, 'UPLOADS'=>'./', @@ -2500,8 +2517,7 @@ protected function sandbox(array $hive=NULL) { function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { $fw=Base::instance(); $cache=Cache::instance(); - $cached=$cache->exists($hash=$fw->hash($file),$data); - if ($cached && $cached[0]+$ttl>microtime(TRUE)) + if ($cache->exists($hash=$fw->hash($file),$data)) return $data; foreach ($fw->split($fw->get('UI').';./') as $dir) if (is_file($this->view=$fw->fixslashes($dir.$file))) { @@ -2516,7 +2532,7 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { foreach($this->trigger['afterrender'] as $func) $data=$fw->call($func,$data); if ($ttl) - $cache->set($hash,$data); + $cache->set($hash,$data,$ttl); return $data; } user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); @@ -2539,7 +2555,7 @@ class Preview extends View { //! MIME type $mime, //! token filter - $filter = array( + $filter=array( 'esc'=>'$this->esc', 'raw'=>'$this->raw', 'alias'=>'\Base::instance()->alias', @@ -2552,15 +2568,24 @@ class Preview extends View { * @param $str string **/ function token($str) { - return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), + $str=trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), Base::instance()->compile($str))); + if (preg_match('/^(.+)(?split($parts[2]) as $func) + $str=is_string($cmd=$this->filter($func))?$cmd.'('.$str.')': + '\Base::instance()->call('. + '$this->filter(\''.$func.'\'),array('.$str.'))'; + } + return $str; } /** - * register token filter + * Register or get (a specific one or all) token filters * @param string $key - * @param string $func - * @return array + * @param string|closure $func + * @return array|closure|string */ function filter($key=NULL,$func=NULL) { if (!$key) @@ -2578,19 +2603,15 @@ function filter($key=NULL,$func=NULL) { protected function build($node) { $self=$this; return preg_replace_callback( - '/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\n+)?/s', + '/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\n+)?|(\{\*.*?\*\})/s', function($expr) use($self) { if ($expr[1]) return $expr[1]; $str=trim($self->token($expr[2])); - if (preg_match('/^([^|]+?)\h*\|(\h*\w+(?:\h*[,;]\h*\w+)*)/', - $str,$parts)) { - $str=$parts[1]; - foreach (Base::instance()->split($parts[2]) as $func) - $str=$self->filter($func).'('.$str.')'; - } - return ''. - (isset($expr[3])?$expr[3]."\n":''); + return empty($expr[4])? + (''. + (isset($expr[3])?$expr[3]."\n":'')): + ''; }, preg_replace_callback( '/\{~(.+?)~\}/s', @@ -2631,8 +2652,7 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { if (!is_dir($tmp=$fw->get('TEMP'))) mkdir($tmp,Base::MODE,TRUE); foreach ($fw->split($fw->get('UI')) as $dir) { - $cached=$cache->exists($hash=$fw->hash($dir.$file),$data); - if ($cached && $cached[0]+$ttl>microtime(TRUE)) + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) return $data; if (is_file($view=$fw->fixslashes($dir.$file))) { if (!is_file($this->view=($tmp. @@ -2659,7 +2679,7 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { foreach ($this->trigger['afterrender'] as $func) $data = $fw->call($func, $data); if ($ttl) - $cache->set($hash,$data); + $cache->set($hash,$data,$ttl); return $data; } } diff --git a/lib/basket.php b/lib/basket.php index 7445e1434..94e030630 100644 --- a/lib/basket.php +++ b/lib/basket.php @@ -195,7 +195,7 @@ function copyfrom($var) { if (is_string($var)) $var=\Base::instance()->get($var); foreach ($var as $key=>$val) - $this->item[$key]=$val; + $this->set($key,$val); } /** diff --git a/lib/db/cursor.php b/lib/db/cursor.php index c218fa478..baef95313 100644 --- a/lib/db/cursor.php +++ b/lib/db/cursor.php @@ -103,7 +103,7 @@ abstract function copyto($key); /** * Get cursor's equivalent external iterator - * Causes a fatal error in PHP 5.3.5if uncommented + * Causes a fatal error in PHP 5.3.5 if uncommented * return ArrayIterator **/ abstract function getiterator(); @@ -119,7 +119,7 @@ function dry() { /** * Return first record (mapper object) that matches criteria - * @return \DB\Cursor|FALSE + * @return static|FALSE * @param $filter string|array * @param $options array * @param $ttl int @@ -171,8 +171,9 @@ function paginate( * @param $ttl int **/ function load($filter=NULL,array $options=NULL,$ttl=0) { + $this->reset(); return ($this->query=$this->find($filter,$options,$ttl)) && - $this->skip(0)?$this->query[$this->ptr=0]:FALSE; + $this->skip(0)?$this->query[$this->ptr]:FALSE; } /** diff --git a/lib/db/jig/mapper.php b/lib/db/jig/mapper.php index e5b007c6d..f0f3953c9 100644 --- a/lib/db/jig/mapper.php +++ b/lib/db/jig/mapper.php @@ -149,7 +149,7 @@ function($expr) use($self) { /** * Return records that match criteria - * @return \DB\JIG\Mapper[]|FALSE + * @return static[]|FALSE * @param $filter array * @param $options array * @param $ttl int @@ -431,7 +431,7 @@ function copyfrom($var,$func=NULL) { if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) - $this->document[$key]=$val; + $this->set($key,$val); } /** diff --git a/lib/db/jig/session.php b/lib/db/jig/session.php index 7f9a6ffaf..e4dca2fef 100644 --- a/lib/db/jig/session.php +++ b/lib/db/jig/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ function open($path,$name) { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ function close() { * @param $id string **/ function read($id) { - if ($id!=$this->sid) - $this->load(array('@session_id=?',$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); + $this->load(array('@session_id=?',$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ function read($id) { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ function write($id,$data) { **/ 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; } @@ -107,19 +116,27 @@ function cleanup($max) { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + return $this->_csrf; } /** - * Return IP address - * @return string|FALSE - **/ + * Return IP address + * @return string + **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** @@ -127,6 +144,8 @@ function ip() { * @return string|FALSE **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } @@ -135,17 +154,19 @@ function stamp() { * @return string|FALSE **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\Jig * @param $file string * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL) { + function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { parent::__construct($db,$file); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -155,26 +176,14 @@ function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL) { 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'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/lib/db/mongo/mapper.php b/lib/db/mongo/mapper.php index f4ef5170d..e814b2937 100644 --- a/lib/db/mongo/mapper.php +++ b/lib/db/mongo/mapper.php @@ -84,7 +84,7 @@ function clear($key) { /** * Convert array to mapper object - * @return \DB\Mongo\Mapper + * @return static * @param $row array **/ protected function factory($row) { @@ -111,7 +111,7 @@ function cast($obj=NULL) { /** * Build query and execute - * @return \DB\Mongo\Mapper[] + * @return static[] * @param $fields string * @param $filter array * @param $options array @@ -177,7 +177,7 @@ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { /** * Return records that match criteria - * @return \DB\Mongo\Mapper[] + * @return static[] * @param $filter array * @param $options array * @param $ttl int @@ -308,7 +308,7 @@ function copyfrom($var,$func=NULL) { if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) - $this->document[$key]=$val; + $this->set($key,$val); } /** diff --git a/lib/db/mongo/session.php b/lib/db/mongo/session.php index 3d7e1d261..0b510f133 100644 --- a/lib/db/mongo/session.php +++ b/lib/db/mongo/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ function open($path,$name) { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ function close() { * @param $id string **/ function read($id) { - if ($id!=$this->sid) - $this->load(array('session_id'=>$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); + $this->load(array('session_id'=>$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ function read($id) { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ function write($id,$data) { **/ 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; } @@ -107,45 +116,57 @@ function cleanup($max) { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + return $this->_csrf; } /** - * Return IP address - * @return string|FALSE - **/ + * Return IP address + * @return string + **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** - * Return Unix timestamp - * @return string|FALSE - **/ + * Return Unix timestamp + * @return string|FALSE + **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } /** - * Return HTTP user agent - * @return string|FALSE - **/ + * Return HTTP user agent + * @return string + **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\Mongo * @param $table string * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL) { + function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { parent::__construct($db,$table); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -155,26 +176,14 @@ function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL) { 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'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/lib/db/sql.php b/lib/db/sql.php index f4734c3c5..600894464 100644 --- a/lib/db/sql.php +++ b/lib/db/sql.php @@ -91,6 +91,8 @@ function type($val) { return \PDO::PARAM_BOOL; case 'integer': return \PDO::PARAM_INT; + case 'resource': + return \PDO::PARAM_LOB; default: return \PDO::PARAM_STR; } @@ -112,6 +114,8 @@ function value($type,$val) { return (bool)$val; case \PDO::PARAM_STR: return (string)$val; + case \PDO::PARAM_LOB: + return (binary)$val; } } @@ -149,6 +153,11 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { for ($i=0;$i<$count;$i++) { $cmd=$cmds[$i]; $arg=$args[$i]; + // ensure 1-based arguments + if (array_key_exists(0,$arg)) { + array_unshift($arg,''); + unset($arg[0]); + } if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) continue; $now=microtime(TRUE); @@ -198,8 +207,8 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { $this->rollback(); user_error('PDOStatement: '.$error[2],E_USER_ERROR); } - if (preg_match('/^\s*'. - '(?:EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING)\b/is',$cmd) || + if (preg_match('/(?:^[\s\(]*'. + '(?:EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || (preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && $query->columnCount())) { $result=$query->fetchall(\PDO::FETCH_ASSOC); @@ -245,11 +254,14 @@ function count() { } /** - * Return SQL profiler results + * Return SQL profiler results (or disable logging) + * @param $flag bool * @return string **/ - function log() { - return $this->log; + function log($flag=TRUE) { + if ($flag) + return $this->log; + $this->log=FALSE; } /** @@ -333,7 +345,10 @@ function schema($table,$fields=NULL,$ttl=0) { \PDO::PARAM_INT: (preg_match('/bool/i',$row[$val[2]])? \PDO::PARAM_BOOL: - \PDO::PARAM_STR), + (preg_match('/blob|bytea|image|binary/i', + $row[$val[2]])? + \PDO::PARAM_LOB: + \PDO::PARAM_STR)), 'default'=>is_string($row[$val[3]])? preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', $row[$val[3]]):$row[$val[3]], @@ -422,7 +437,7 @@ function quotekey($key) { } /** - * Redirect call to MongoDB object + * Redirect call to PDO object * @return mixed * @param $func string * @param $args array diff --git a/lib/db/sql/mapper.php b/lib/db/sql/mapper.php index 3a13c65b4..ca6f8493c 100644 --- a/lib/db/sql/mapper.php +++ b/lib/db/sql/mapper.php @@ -190,7 +190,7 @@ function($row) { /** * Build query string and execute - * @return \DB\SQL\Mapper[] + * @return static[] * @param $fields string * @param $filter string|array * @param $options array @@ -294,7 +294,7 @@ function($str) use($db) { /** * Return records that match criteria - * @return \DB\SQL\Mapper[] + * @return static[] * @param $filter string|array * @param $options array * @param $ttl int @@ -362,7 +362,7 @@ function skip($ofs=1) { $field['value']=$dry?NULL:$out->adhoc[$key]['value']; unset($field); } - if (isset($this->trigger['load'])) + if (!$dry && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); return $out; } @@ -559,14 +559,8 @@ function copyfrom($var,$func=NULL) { if ($func) $var=call_user_func($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); - } + if (in_array($key,array_keys($this->fields))) + $this->set($key,$val); } /** diff --git a/lib/db/sql/session.php b/lib/db/sql/session.php index 12c27f425..607020500 100644 --- a/lib/db/sql/session.php +++ b/lib/db/sql/session.php @@ -27,7 +27,15 @@ class Session extends Mapper { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -44,6 +52,8 @@ function open($path,$name) { * @return TRUE **/ function close() { + $this->reset(); + $this->sid=NULL; return TRUE; } @@ -53,9 +63,20 @@ function close() { * @param $id string **/ function read($id) { - if ($id!=$this->sid) - $this->load(array('session_id=?',$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); + $this->load(array('session_id=?',$this->sid=$id)); + if ($this->dry()) + return FALSE; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $this->get('data'); } /** @@ -65,19 +86,10 @@ function read($id) { * @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('ip',$this->_ip); + $this->set('agent',$this->_agent); $this->set('stamp',time()); $this->save(); return TRUE; @@ -90,9 +102,6 @@ function write($id,$data) { **/ 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; } @@ -106,20 +115,28 @@ function cleanup($max) { return TRUE; } + /** + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + /** * Return anti-CSRF token - * @return string|FALSE + * @return string **/ function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); + return $this->_csrf; } /** * Return IP address - * @return string|FALSE + * @return string **/ function ip() { - return $this->dry()?FALSE:$this->get('ip'); + return $this->_ip; } /** @@ -127,25 +144,28 @@ function ip() { * @return string|FALSE **/ function stamp() { + if (!$this->sid) + session_start(); return $this->dry()?FALSE:$this->get('stamp'); } /** * Return HTTP user agent - * @return string|FALSE + * @return string **/ function agent() { - return $this->dry()?FALSE:$this->get('agent'); + return $this->_agent; } /** * Instantiate class - * @param $db object + * @param $db \DB\SQL * @param $table string * @param $force bool * @param $onsuspect callback + * @param $key string **/ - function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) { + function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL) { if ($force) { $eol="\n"; $tab="\t"; @@ -160,7 +180,6 @@ function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) $table.' ('.$eol. $tab.$db->quotekey('session_id').' VARCHAR(40),'.$eol. $tab.$db->quotekey('data').' TEXT,'.$eol. - $tab.$db->quotekey('csrf').' TEXT,'.$eol. $tab.$db->quotekey('ip').' VARCHAR(40),'.$eol. $tab.$db->quotekey('agent').' VARCHAR(255),'.$eol. $tab.$db->quotekey('stamp').' INTEGER,'.$eol. @@ -169,6 +188,7 @@ function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) ); } parent::__construct($db,$table); + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -178,26 +198,14 @@ function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) 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'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $this->_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(); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/lib/image.php b/lib/image.php index 55a29ba98..e1acd4832 100644 --- a/lib/image.php +++ b/lib/image.php @@ -230,9 +230,9 @@ function resize($width,$height,$crop=TRUE,$enlarge=TRUE) { $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); if (!$crop) { if ($width/$ratio<=$height) - $height=$width/$ratio; + $height=round($width/$ratio); else - $width=$height*$ratio; + $width=round($height*$ratio); } if (!$enlarge) { $width=min($origw,$width); @@ -245,12 +245,12 @@ function resize($width,$height,$crop=TRUE,$enlarge=TRUE) { // Resize if ($crop) { if ($width/$ratio<=$height) { - $cropw=$origh*$width/$height; + $cropw=round($origh*$width/$height); imagecopyresampled($tmp,$this->data, 0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh); } else { - $croph=$origw*$height/$width; + $croph=round($origw*$height/$width); imagecopyresampled($tmp,$this->data, 0,0,0,($origh-$croph)/2,$width,$height,$origw,$croph); } @@ -489,6 +489,14 @@ function dump() { return ob_get_clean(); } + /** + * Return image resource + * @return resource + **/ + function data() { + return $this->data; + } + /** * Save current state * @return object diff --git a/lib/session.php b/lib/session.php index 8723c69f2..8b28c936c 100644 --- a/lib/session.php +++ b/lib/session.php @@ -25,7 +25,15 @@ class Session { protected //! Session ID - $sid; + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; /** * Open session @@ -42,6 +50,7 @@ function open($path,$name) { * @return TRUE **/ function close() { + $this->sid=NULL; return TRUE; } @@ -51,9 +60,20 @@ function close() { * @param $id string **/ function read($id) { - if ($id!=$this->sid) - $this->sid=$id; - return Cache::instance()->exists($id.'.@',$data)?$data['data']:FALSE; + $this->sid=$id; + if (!$data=Cache::instance()->get($id.'.@')) + return FALSE; + if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { + $fw=Base::instance(); + if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + $fw->clear('COOKIE.'.session_name()); + $fw->error(403); + } + } + return $data['data']; } /** @@ -64,20 +84,12 @@ function read($id) { **/ function write($id,$data) { $fw=Base::instance(); - $sent=headers_sent(); - $headers=$fw->get('HEADERS'); - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); $jar=$fw->get('JAR'); - if ($id!=$this->sid) - $this->sid=$id; Cache::instance()->set($id.'.@', array( 'data'=>$data, - 'csrf'=>$sent?$this->csrf():$csrf, - 'ip'=>$fw->get('IP'), - 'agent'=>isset($headers['User-Agent'])? - $headers['User-Agent']:'', + 'ip'=>$this->_ip, + 'agent'=>$this->_agent, 'stamp'=>time() ), $jar['expire']?($jar['expire']-time()):0 @@ -92,9 +104,6 @@ function write($id,$data) { **/ function destroy($id) { Cache::instance()->clear($id.'.@'); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); return TRUE; } @@ -109,50 +118,55 @@ function cleanup($max) { } /** - * Return anti-CSRF token - * @return string|FALSE - **/ + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ function csrf() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['csrf']:FALSE; + return $this->_csrf; } /** - * Return IP address - * @return string|FALSE - **/ + * Return IP address + * @return string + **/ function ip() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['ip']:FALSE; + return $this->_ip; } /** - * Return Unix timestamp - * @return string|FALSE - **/ + * Return Unix timestamp + * @return string|FALSE + **/ function stamp() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['stamp']:FALSE; + if (!$this->sid) + session_start(); + return Cache::instance()->exists($this->sid.'.@',$data)? + $data['stamp']:FALSE; } /** - * Return HTTP user agent - * @return string|FALSE - **/ + * Return HTTP user agent + * @return string + **/ function agent() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['agent']:FALSE; + return $this->_agent; } /** * Instantiate class * @param $onsuspect callback + * @param $key string **/ - function __construct($onsuspect=NULL) { + function __construct($onsuspect=NULL,$key=NULL) { + $this->onsuspect=$onsuspect; session_set_save_handler( array($this,'open'), array($this,'close'), @@ -162,30 +176,14 @@ function __construct($onsuspect=NULL) { 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'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. $fw->hash(mt_rand()); - $jar=$fw->get('JAR'); - if (Cache::instance()->exists(($this->sid=session_id()).'.@',$data)) { - $data['csrf']=$csrf; - Cache::instance()->set($this->sid.'.@', - $data, - $jar['expire']?($jar['expire']-time()):0 - ); - } + if ($key) + $fw->set($key,$this->_csrf); + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->get('IP'); } } diff --git a/lib/smtp.php b/lib/smtp.php index f0351ae5e..3c9e964ff 100644 --- a/lib/smtp.php +++ b/lib/smtp.php @@ -182,12 +182,13 @@ function send($message,$log=TRUE) { stream_socket_enable_crypto( $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); - if (preg_match('/8BITMIME/',$reply)) - $headers['Content-Transfer-Encoding']='8bit'; - else { - $headers['Content-Transfer-Encoding']='quoted-printable'; - $message=quoted_printable_encode($message); - } + } + if (preg_match('/8BITMIME/',$reply)) + $headers['Content-Transfer-Encoding']='8bit'; + else { + $headers['Content-Transfer-Encoding']='quoted-printable'; + $message=preg_replace('/^\.(.+)/m', + '..$1',quoted_printable_encode($message)); } if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { // Authenticate @@ -204,9 +205,9 @@ function send($message,$log=TRUE) { $str=''; // Stringify headers foreach ($headers as $key=>&$val) { - if (!in_array($key,$reqd)) { + if (!in_array($key,$reqd) && (!$this->attachments || + $key!='Content-Type' && $key!='Content-Transfer-Encoding')) $str.=$key.': '.$val.$eol; - } if (in_array($key,array('From','To','Cc','Bcc')) && !preg_match('/[<>]/',$val)) $val='<'.$val.'>'; @@ -221,12 +222,13 @@ function send($message,$log=TRUE) { $this->dialog('DATA',$log); if ($this->attachments) { // Replace Content-Type - $hash=uniqid(NULL,TRUE); $type=$headers['Content-Type']; - $headers['Content-Type']='multipart/mixed; '. - 'boundary="'.$hash.'"'; + unset($headers['Content-Type']); + $enc=$headers['Content-Transfer-Encoding']; + unset($headers['Content-Transfer-Encoding']); + $hash=uniqid(NULL,TRUE); // Send mail headers - $out=''; + $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; foreach ($headers as $key=>$val) if ($key!='Bcc') $out.=$key.': '.$val.$eol; @@ -235,16 +237,17 @@ function send($message,$log=TRUE) { $out.=$eol; $out.='--'.$hash.$eol; $out.='Content-Type: '.$type.$eol; - $out.=$eol; + $out.='Content-Transfer-Encoding: '.$enc.$eol; + $out.=$str.$eol; $out.=$message.$eol; foreach ($this->attachments as $attachment) { if (is_array($attachment['filename'])) { - list($alias,$file)=each($attachment); + list($alias,$file)=each($attachment['filename']); $filename=$alias; $attachment['filename']=$file; } else - $filename=basename($attachment); + $filename=basename($attachment['filename']); $out.='--'.$hash.$eol; $out.='Content-Type: application/octet-stream'.$eol; $out.='Content-Transfer-Encoding: base64'.$eol; @@ -253,8 +256,8 @@ function send($message,$log=TRUE) { $out.='Content-Disposition: attachment; '. 'filename="'.$filename.'"'.$eol; $out.=$eol; - $out.=chunk_split( - base64_encode(file_get_contents($attachment))).$eol; + $out.=chunk_split(base64_encode( + file_get_contents($attachment['filename']))).$eol; } $out.=$eol; $out.='--'.$hash.'--'.$eol; @@ -287,7 +290,7 @@ function send($message,$log=TRUE) { * @param $user string * @param $pw string **/ - function __construct($host,$port,$scheme,$user,$pw) { + function __construct($host='localhost',$port=25,$scheme=null,$user=null,$pw=null) { $this->headers=array( 'MIME-Version'=>'1.0', 'Content-Type'=>'text/plain; '. diff --git a/lib/template.php b/lib/template.php index 8c1c0e11e..8b985cde7 100644 --- a/lib/template.php +++ b/lib/template.php @@ -69,6 +69,7 @@ protected function _include(array $node) { \Base::instance()->stringify($pair[2])); },$pairs)).')+get_defined_vars()': 'get_defined_vars()'; + $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; return 'token($attrib['if']).') '):''). @@ -76,7 +77,7 @@ protected function _include(array $node) { (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? $this->token($attrib['href']): Base::instance()->stringify($attrib['href'])).','. - '$this->mime,'.$hive.'); ?>'); + '$this->mime,'.$hive.','.$ttl.'); ?>'); } /** @@ -269,45 +270,40 @@ function __call($func,array $args) { **/ function parse($text) { // Build tree structure - for ($ptr=0,$len=strlen($text),$tree=array(),$node=&$tree, - $stack=array(),$depth=0,$tmp='';$ptr<$len;) - if (preg_match('/^<(\/?)(?:F3:)?'. + for ($ptr=0,$w=5,$len=strlen($text),$tree=array(),$tmp='';$ptr<$len;) + if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. '('.$this->tags.')\b((?:\h+[\w-]+'. - '(?:\h*=\h*(?:"(?:.+?)"|\'(?:.+?)\'))?|'. + '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. '\h*\{\{.+?\}\})*)\h*(\/?)>/is', substr($text,$ptr),$match)) { - if (strlen($tmp)) - $node[]=$tmp; + if (strlen($tmp)||$match[1]) + $tree[]=$tmp.$match[1]; // Element node - if ($match[1]) { + if ($match[2]) { // Find matching start tag - $save=$depth; - $found=FALSE; - while ($depth>0) { - $depth--; - foreach ($stack[$depth] as $item) - if (is_array($item) && isset($item[$match[2]])) { - // Start tag found - $found=TRUE; - break 2; - } + $stack=array(); + for($i=count($tree)-1;$i>=0;$i--) { + $item = $tree[$i]; + if (is_array($item) && array_key_exists($match[3],$item) + && !isset($item[$match[3]][0])) { + // Start tag found + $tree[$i][$match[3]]+=array_reverse($stack); + $tree=array_slice($tree,0,$i+1); + break; + } else $stack[]=$item; } - if (!$found) - // Unbalanced tag - $depth=$save; - $node=&$stack[$depth]; } else { // Start tag - $stack[$depth]=&$node; - $node=&$node[][$match[2]]; - if ($match[3]) { + $node=&$tree[][$match[3]]; + $node=array(); + if ($match[4]) { // Process attributes preg_match_all( '/(?:\b([\w-]+)\h*'. '(?:=\h*(?:"(.*?)"|\'(.*?)\'))?|'. '(\{\{.+?\}\}))/s', - $match[3],$attr,PREG_SET_ORDER); + $match[4],$attr,PREG_SET_ORDER); foreach ($attr as $kv) if (isset($kv[4])) $node['@attrib'][]=$kv[4]; @@ -318,26 +314,23 @@ function parse($text) { (isset($kv[3]) && $kv[3]!==''? $kv[3]:NULL)); } - if ($match[4]) - // Empty tag - $node=&$stack[$depth]; - else - $depth++; } $tmp=''; $ptr+=strlen($match[0]); + $w=5; } else { // Text node - $tmp.=substr($text,$ptr,1); - $ptr++; + $tmp.=substr($text,$ptr,$w); + $ptr+=$w; + if ($w<50) + $w++; } if (strlen($tmp)) // Append trailing text - $node[]=$tmp; + $tree[]=$tmp; // Break references unset($node); - unset($stack); return $tree; } diff --git a/lib/web.php b/lib/web.php index ab3389ecb..57fbf937e 100644 --- a/lib/web.php +++ b/lib/web.php @@ -133,7 +133,7 @@ function send($file,$mime=NULL,$kbps=0,$force=TRUE) { header('Content-Type: '.($mime?:$this->mime($file))); if ($force) header('Content-Disposition: attachment; '. - 'filename='.var_export(basename($file),TRUE)); + 'filename="'.basename($file).'"'); header('Accept-Ranges: bytes'); header('Content-Length: '.$size); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); @@ -259,10 +259,13 @@ function progress($id) { **/ protected function _curl($url,$options) { $curl=curl_init($url); - curl_setopt($curl,CURLOPT_FOLLOWLOCATION, - $options['follow_location']); + if (!ini_get('open_basedir')) + curl_setopt($curl,CURLOPT_FOLLOWLOCATION, + $options['follow_location']); curl_setopt($curl,CURLOPT_MAXREDIRS, $options['max_redirects']); + curl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']); if (isset($options['header'])) curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); @@ -288,6 +291,11 @@ function($curl,$line) use(&$headers) { curl_exec($curl); curl_close($curl); $body=ob_get_clean(); + if ($options['follow_location'] && + preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { + $options['max_redirects']--; + return $this->request($loc[1],$options); + } return array( 'body'=>$body, 'headers'=>$headers, @@ -357,7 +365,8 @@ protected function _socket($url,$options) { if (!$socket) return FALSE; stream_set_blocking($socket,TRUE); - stream_set_timeout($socket,$options['timeout']); + stream_set_timeout($socket,isset($options['timeout'])? + $options['timeout']:ini_get('default_socket_timeout')); fputs($socket,$options['method'].' '.$parts['path']. ($parts['query']?('?'.$parts['query']):'').' HTTP/1.0'.$eol ); @@ -565,7 +574,7 @@ function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { $src=$fw->read($save); for ($ptr=0,$len=strlen($src);$ptr<$len;) { if (preg_match('/^@import\h+url'. - '\(\h*([\'"])(.+?)\1\h*\)[^;]*;/', + '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/', substr($src,$ptr),$parts)) { $path=dirname($file); $data.=$this->minify( @@ -586,7 +595,8 @@ function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { // Single-line comment $str=strstr( substr($src,$ptr+2),"\n",TRUE); - $ptr+=strlen($str)+2; + $ptr+=(empty($str))? + strlen(substr($src,$ptr)):strlen($str)+2; } else { // Presume it's a regex pattern @@ -785,7 +795,7 @@ function slug($text) { 'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u', 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', - 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh' + 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'' )+Base::instance()->get('DIACRITICS'))))),'-'); }