diff --git a/app/config.ini b/app/config.ini index cc4bbf54..3904003a 100644 --- a/app/config.ini +++ b/app/config.ini @@ -6,17 +6,20 @@ SERVER_NAME = PATHFINDER [globals] ; Default Verbosity level of the stack trace. ; Assign values between 0 to 3 for increasing verbosity levels. Check (environment.ini) config for overwriting +; (default: 0) DEBUG = 0 ; If TRUE, the framework, after having logged stack trace and errors, stops execution -; -> (die without any status) when a non-fatal error is detected. +; -> (die without any status) when a non-fatal error is detected. (default: FALSE) HALT = FALSE -; Timezone to use. Sync program with eve server time +; Timezone to use. Sync program with eve server time. (default: UTC) TZ = UTC -; Cache backend. Can handle Memcache module, APC, WinCache, XCache and a filesystem-based cache. -CACHE = TRUE +; Cache backend. Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache. +; (default: folder=tmp/cache/) +CACHE = folder=tmp/cache/ +;CACHE = redis=localhost:6379 ; Callback functions ============================================================================== ONERROR = Controller\Controller->showError @@ -26,25 +29,25 @@ UNLOAD = Controller\Controller->unload ; Path configurations ============================================================================= ; relative to "BASE" dir -; Temporary folder for cache, filesystem locks, compiled F3 templates, etc. +; Temporary folder for cache, filesystem locks, compiled F3 templates, etc. (default: tmp/) TEMP = tmp/ -; Log file folder +; Log file folder. (default: logs/) LOGS = logs/ -; UI/template folder +; UI/template folder. (default: public/) UI = public/ -; Autoloader for user-defined PHP classes that the framework will attempt to autoload at runtime +; Autoloader for user-defined PHP classes that the framework will attempt to autoload at runtime. (default: app/main/) AUTOLOAD = app/main/ -; Favicons +; Favicons. (default: favicon/) FAVICON = favicon/ -; Export folder (e.g. static table data) +; Export folder (e.g. static table data). (default: export/) EXPORT = export/ -; Default language (overwrites HTTP Accept-Language request header) used for "setlocale()" affects number formatting +; Default language (overwrites HTTP Accept-Language request header) used for "setlocale()" affects number formatting. (default: en-US) LANGUAGE = en-US ; load additional config files diff --git a/app/environment.ini b/app/environment.ini index d12081bf..d9842588 100644 --- a/app/environment.ini +++ b/app/environment.ini @@ -8,7 +8,7 @@ SERVER = DEVELOP [ENVIRONMENT.DEVELOP] ; path to index.php (Default: leave blank == "auto-detect") -; -> e.g. set pathfinder/ if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) +; -> e.g. set /pathfinder if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) BASE = ; deployment URL (e.g. http://localhost) URL = http://pathfinder.local @@ -45,9 +45,14 @@ SMTP_PASS = root SMTP_FROM = pathfinder@localhost.com SMTP_ERROR = pathfinder@localhost.com +; TCP Socket configuration (optional) (advanced) +;SOCKET_HOST = 127.0.0.1 +;SOCKET_PORT = 5555 + + [ENVIRONMENT.PRODUCTION] ; path to index.php (Default: leave blank == "auto-detect") -; -> e.g. set pathfinder/ if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) +; -> e.g. set /pathfinder if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) BASE = ; deployment URL (e.g. https://www.pathfinder-w.space) URL = https://www.pathfinder-w.space @@ -83,3 +88,7 @@ SMTP_PASS = SMTP_FROM = registration@pathfinder-w.space SMTP_ERROR = admin@pathfinder-w.space + +; TCP Socket configuration (optional) (advanced) +;SOCKET_HOST = 127.0.0.1 +;SOCKET_PORT = 5555 \ No newline at end of file diff --git a/app/lib/CHANGELOG b/app/lib/CHANGELOG index cf2dab51..f831d7d6 100644 --- a/app/lib/CHANGELOG +++ b/app/lib/CHANGELOG @@ -1,5 +1,112 @@ CHANGELOG +3.6.0 (19 November 2016) +* NEW: [cli] request type +* NEW: console-friendly CLI mode +* NEW: lexicon caching +* NEW: Silent operator skips startup error check (#125) +* NEW: DB\SQL->trans() +* NEW: custom config section parser, i.e. [conf > Foo::bar] +* NEW: support for cache tags in SQL +* NEW: custom FORMATS +* NEW: Mongo mapper fields whitelist +* NEW: WebSocket server +* NEW: Base->extend method (#158) +* NEW: Implement framework variable caching via config, i.e. FOO = "bar" | 3600 +* NEW: Lightweight OAuth2 client +* NEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629) +* NEW: CLI variable +* NEW: Web->send, specify custom filename (#124) +* NEW: Web->send, added flushing flag (#131) +* NEW: Indexed route wildcards, now exposed in PARAMS['*'] +* Changed: PHP 5.4 is now the minimum version requirement +* Changed: Prevent database wrappers from being cloned +* Changed: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string +* Changed: Removed ALIASES autobuilding (#118) +* Changed: Route wildcards match empty strings (#119) +* Changed: Disable default debug highlighting, HIGHLIGHT is false now +* General PHP 5.4 optimizations +* Optimized config parsing +* Optimized Base->recursive +* Optimized header extraction +* Optimized cache/expire headers +* Optimized session_start behaviour (bcosca/fatfree#673) +* Optimized reroute regex +* Tweaked cookie removal +* Better route precedence order +* Performance tweak: reduced cache calls +* Refactored lexicon (LOCALES) build-up, much faster now +* Added turkish locale bug workaround +* Geo->tzinfo Update to UTC +* Added Xcache reset (bcosca/fatfree#928) +* Redis cache: allow db name in dsn +* SMTP: Improve server emulation responses +* SMTP: Optimize transmission envelope +* SMTP: Implement mock transmission +* SMTP: Various bug fixes and feature improvements +* SMTP: quit on failed authentication +* Geo->weather: force metric units +* Base->until: Implement CLI interoperability +* Base->format: looser plural syntax +* Base->format: Force decimal as default number format +* Base->merge: Added $keep flag to save result to the hive key +* Base->reroute: Allow array as URL argument for aliasing +* Base->alias: Allow query string (or array) to be appended to alias +* Permit reroute to named routes with URL query segment +* Sync COOKIE global on set() +* Permit non-hive variables to use JS dot notation +* RFC2616: Use absolute URIs for Location header +* Matrix->calendar: Check if calendar extension is loaded +* Markdown: require start of line/whitespace for text processing (#136) +* DB\[SQL|Jig|Mongo]->log(FALSE) disables logging +* DB\SQL->exec: Added timestamp toggle to db log +* DB\SQL->schema: Remove unnecessary line terminators +* DB\SQL\Mapper: allow array filter with empty string +* DB\SQL\Mapper: optimized handling for key-less tables +* DB\SQL\Mapper: added float support (#106) +* DB\SQL\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950) +* Web: Catch cURL errors +* Optimize Web->receive (bcosca/fatfree#930) +* Web->minify: fix arbitrary file download vulnerability +* Web->request: fix cache control max-age detection (bcosca/fatfree#908) +* Web->request: Add request headers & error message to return value (bcosca/fatfree#737) +* Web->request: Refactored response to HTTP request +* Web->send flush while sending big files +* Image->rgb: allow hex strings +* Image->captcha: Check if GD module supports TrueType +* Image->load: Return FALSE on load failure +* Image->resize: keep aspect ratio when only width or height was given +* Updated OpenID lib (bcosca/fatfree#965) +* Audit->card: add new mastercard "2" BIN range (bcosca/fatfree#954) +* Deprecated: Bcrypt class +* Preview->render: optimized detection to remove short open PHP tags and allow xml tags (#133) +* Display file and line number in exception handler (bcosca/fatfree#967) +* Added error reporting level to Base->error and ERROR.level (bcosca/fatfree#957) +* Added optional custom cache instance to Session (#141) +* CLI-aware mock() +* XFRAME and PACKAGE can be switched off now (#128) +* Bug fix: wrong time calculation on memcache reset (#170) +* Bug fix: encode CLI parameters +* Bug fix: Close connection on abort explicitly (#162) +* Bug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault) +* Bug fix: Magic->offsetset, access property as array element (#147) +* Bug fix: multi-line custom template tag parsing (bcosca/fatfree#935) +* Bug fix: cache headers on errors (bcosca/fatfree#885) +* Bug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl +* Bug fix: Web, Invalid user error constant (bcosca/fatfree#962) +* Bug fix: Web->request, redirections for domain-less location (#135) +* Bug fix: DB\SQL\Mapper, reset changed flag after update (#142, #152) +* Bug fix: DB\SQL\Mapper, fix changed flag when using assignment operator #143 #150 #151 +* Bug fix: DB\SQL\Mapper, revival of the HAVING clause +* Bug fix: DB\SQL\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916) +* Bug fix: DB\SQL\Session, quote table name (bcosca/fatfree#977) +* Bug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927) +* Bug fix: mock does not populate PATH variable +* Bug fix: Geo->weather API key (#129) +* Bug fix: Incorrect compilation of array element with zero index +* Bug fix: Compilation of array construct is incorrect +* Bug fix: Trailing slash redirection on UTF-8 paths (#121) + 3.5.1 (31 December 2015) * NEW: ttl attribute in template tag * NEW: allow anonymous function for template filter diff --git a/app/lib/audit.php b/app/lib/audit.php index 1338ca8c..4c91740d 100644 --- a/app/lib/audit.php +++ b/app/lib/audit.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -47,7 +47,7 @@ class Audit extends Prefab { * @param $mx boolean **/ function email($str,$mx=TRUE) { - $hosts=array(); + $hosts=[]; return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) && (!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts)); } @@ -166,7 +166,8 @@ class Audit extends Prefab { return 'Discover'; if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id)) return 'JCB'; - if (preg_match('/^5[1-5][0-9]{14}$/',$id)) + if (preg_match('/^5[1-5][0-9]{14}$|'. + '^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$id)) return 'MasterCard'; if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id)) return 'Visa'; diff --git a/app/lib/auth.php b/app/lib/auth.php index 5f7e0508..56f89ed5 100644 --- a/app/lib/auth.php +++ b/app/lib/auth.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -20,7 +20,6 @@ */ - //! Authorization/authentication plug-in class Auth { @@ -48,19 +47,19 @@ class Auth { protected function _jig($id,$pw,$realm) { return (bool) call_user_func_array( - array($this->mapper,'load'), - array( + [$this->mapper,'load'], + [ array_merge( - array( + [ '@'.$this->args['id'].'==? AND '. '@'.$this->args['pw'].'==?'. (isset($this->args['realm'])? (' AND @'.$this->args['realm'].'==?'):''), $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) + ], + (isset($this->args['realm'])?[$realm]:[]) ) - ) + ] ); } @@ -74,12 +73,12 @@ class Auth { protected function _mongo($id,$pw,$realm) { return (bool) $this->mapper->load( - array( + [ $this->args['id']=>$id, $this->args['pw']=>$pw - )+ + ]+ (isset($this->args['realm'])? - array($this->args['realm']=>$realm):array()) + [$this->args['realm']=>$realm]:[]) ); } @@ -93,19 +92,19 @@ class Auth { protected function _sql($id,$pw,$realm) { return (bool) call_user_func_array( - array($this->mapper,'load'), - array( + [$this->mapper,'load'], + [ array_merge( - array( + [ $this->args['id'].'=? AND '. $this->args['pw'].'=?'. (isset($this->args['realm'])? (' AND '.$this->args['realm'].'=?'):''), $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) + ], + (isset($this->args['realm'])?[$realm]:[]) ) - ) + ] ); } diff --git a/app/lib/base.php b/app/lib/base.php index ea1a1f09..b5a7bdca 100644 --- a/app/lib/base.php +++ b/app/lib/base.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess { //@{ Framework details const PACKAGE='Fat-Free Framework', - VERSION='3.5.1-Release'; + VERSION='3.6.0-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -102,10 +102,11 @@ final class Base extends Prefab implements ArrayAccess { //! Syntax highlighting stylesheet CSS='code.css'; - //@{ HTTP request types + //@{ Request types const REQ_SYNC=1, - REQ_AJAX=2; + REQ_AJAX=2, + REQ_CLI=4; //@} //@{ Error messages @@ -153,26 +154,30 @@ final class Base extends Prefab implements ArrayAccess { * Replace tokenized URL with available token values * @return string * @param $url array|string - * @param $params array + * @param $args array **/ - function build($url,$params=array()) { - $params+=$this->hive['PARAMS']; + function build($url,$args=[]) { + $args+=$this->hive['PARAMS']; if (is_array($url)) foreach ($url as &$var) { - $var=$this->build($var,$params); + $var=$this->build($var,$args); unset($var); } else { $i=0; - $url=preg_replace_callback('/@(\w+)|\*/', - function($match) use(&$i,$params) { - $i++; + $url=preg_replace_callback('/@(\w+)|(\*)/', + function($match) use(&$i,$args) { if (isset($match[1]) && - array_key_exists($match[1],$params)) - return $params[$match[1]]; - return array_key_exists($i,$params)? - $params[$i]: - $match[0]; + array_key_exists($match[1],$args)) + return $args[$match[1]]; + if (isset($match[2]) && + array_key_exists($match[2],$args)) { + if (!is_array($args[$match[2]])) + return $args[$match[2]]; + $i++; + return $args[$match[2]][$i-1]; + } + return $match[0]; },$url); } return $url; @@ -183,14 +188,17 @@ final class Base extends Prefab implements ArrayAccess { * @return string * @param $name string * @param $params array|string + * @param $query string|array **/ - function alias($name,$params=array()) { + function alias($name,$params=[],$query=NULL) { if (!is_array($params)) $params=$this->parse($params); if (empty($this->hive['ALIASES'][$name])) user_error(sprintf(self::E_Named,$name),E_USER_ERROR); $url=$this->build($this->hive['ALIASES'][$name],$params); - return $url; + if (is_array($query)) + $query=http_build_query($query); + return $url.($query?('?'.$query):''); } /** @@ -199,11 +207,17 @@ final class Base extends Prefab implements ArrayAccess { * @param $str string **/ function parse($str) { - preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', + preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', $str,$pairs,PREG_SET_ORDER); - $out=array(); + $out=[]; foreach ($pairs as $pair) - $out[$pair[1]]=trim($pair[2]); + if ($pair[2]) { + $out[$pair[1]]=[]; + foreach (explode(',',$pair[2]) as $val) + array_push($out[$pair[1]],$val); + } + else + $out[$pair[1]]=trim($pair[3]); return $out; } @@ -224,12 +238,17 @@ final class Base extends Prefab implements ArrayAccess { ((function_exists($expr[1])? ('.'.$expr[1]): ('['.var_export($expr[1],TRUE).']')).'('): - ('['.var_export( - isset($expr[3])? - trim($fw->compile($expr[3])): - (ctype_digit($expr[2])? + ('['. + (isset($expr[3])? + (strlen($expr[3])? + var_export( + trim($fw->compile($expr[3])), + TRUE): + ''): + var_export(ctype_digit($expr[2])? (int)$expr[2]: - $expr[2]),TRUE).']'); + $expr[2],TRUE)). + ']'); }, $var[1] ); @@ -244,21 +263,25 @@ final class Base extends Prefab implements ArrayAccess { * @return mixed * @param $key string * @param $add bool + * @param $var mixed **/ - function &ref($key,$add=TRUE) { + function &ref($key,$add=TRUE,&$var=NULL) { $null=NULL; $parts=$this->cut($key); if ($parts[0]=='SESSION') { - @session_start(); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); $this->sync('SESSION'); } elseif (!preg_match('/^\w+$/',$parts[0])) user_error(sprintf(self::E_Hive,$this->stringify($key)), E_USER_ERROR); - if ($add) - $var=&$this->hive; - else - $var=$this->hive; + if (is_null($var)) { + if ($add) + $var=&$this->hive; + else + $var=$this->hive; + } $obj=FALSE; foreach ($parts as $part) if ($part=='->') @@ -276,7 +299,7 @@ final class Base extends Prefab implements ArrayAccess { } else { if (!is_array($var)) - $var=array(); + $var=[]; if ($add || array_key_exists($part,$var)) $var=&$var[$part]; else { @@ -284,8 +307,6 @@ final class Base extends Prefab implements ArrayAccess { break; } } - if ($parts[0]=='ALIASES') - $var=$this->build($var); return $var; } @@ -330,9 +351,15 @@ final class Base extends Prefab implements ArrayAccess { if ($expr[1]=='COOKIE') { $parts=$this->cut($key); $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + if (isset($_COOKIE[$parts[1]])) { + $jar['expire']=strtotime('-1 year'); + call_user_func_array('setcookie', + array_merge([$parts[1],NULL],$jar)); + } if ($ttl) $jar['expire']=$time+$ttl; - call_user_func_array('setcookie',array($parts[1],$val)+$jar); + call_user_func_array('setcookie',[$parts[1],$val]+$jar); + $_COOKIE[$parts[1]]=$val; return $val; } } @@ -351,10 +378,14 @@ final class Base extends Prefab implements ArrayAccess { case 'LANGUAGE': if (!isset($lang)) $val=$this->language($val); - $lex=$this->lexicon($this->hive['LOCALES']); + $lex=$this->lexicon($this->hive['LOCALES'],$ttl); case 'LOCALES': - if (isset($lex) || $lex=$this->lexicon($val)) - $this->mset($lex,$this->hive['PREFIX'],$ttl); + if (isset($lex) || $lex=$this->lexicon($val,$ttl)) + foreach ($lex as $dt=>$dd) { + $ref=&$this->ref($this->hive['PREFIX'].$dt); + $ref=$dd; + unset($ref); + } break; case 'TZ': date_default_timezone_set($val); @@ -367,10 +398,9 @@ final class Base extends Prefab implements ArrayAccess { $jar['expire']-=$time; call_user_func_array('session_set_cookie_params',$jar); } - $cache=Cache::instance(); - if ($cache->exists($hash=$this->hash($key).'.var') || $ttl) + if ($ttl) // Persist the key-value pair - $cache->set($hash,$val,$ttl); + Cache::instance()->set($this->hash($key).'.var',$val,$ttl); return $ref; } @@ -383,8 +413,8 @@ final class Base extends Prefab implements ArrayAccess { function get($key,$args=NULL) { if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) return call_user_func_array( - array($this,'format'), - array_merge(array($val),is_array($args)?$args:array($args)) + [$this,'format'], + array_merge([$val],is_array($args)?$args:[$args]) ); if (is_null($val)) { // Attempt to retrieve from cache @@ -413,12 +443,13 @@ final class Base extends Prefab implements ArrayAccess { $jar=$this->hive['JAR']; $jar['expire']=strtotime('-1 year'); call_user_func_array('setcookie', - array_merge(array($parts[1],''),$jar)); + array_merge([$parts[1],NULL],$jar)); unset($_COOKIE[$parts[1]]); } } elseif ($parts[0]=='SESSION') { - @session_start(); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); if (empty($parts[1])) { // End session session_unset(); @@ -571,10 +602,33 @@ final class Base extends Prefab implements ArrayAccess { * @return array * @param $key string * @param $src string|array + * @param $keep bool **/ - function merge($key,$src) { + function merge($key,$src,$keep=FALSE) { $ref=&$this->ref($key); - return array_merge($ref,is_string($src)?$this->hive[$src]:$src); + if (!$ref) + $ref=[]; + $out=array_merge($ref,is_string($src)?$this->hive[$src]:$src); + if ($keep) + $ref=$out; + return $out; + } + + /** + * Extend hive array variable with default values from $src + * @return array + * @param $key string + * @param $src string|array + * @param $keep bool + **/ + function extend($key,$src,$keep=FALSE) { + $ref=&$this->ref($key); + if (!$ref) + $ref=[]; + $out=array_replace_recursive(is_string($src)?$this->hive[$src]:$src,$ref); + if ($keep) + $ref=$out; + return $out; } /** @@ -610,7 +664,7 @@ final class Base extends Prefab implements ArrayAccess { return '*RECURSION*'; } else - $stack=array(); + $stack=[]; switch (gettype($arg)) { case 'object': $str=''; @@ -618,8 +672,8 @@ final class Base extends Prefab implements ArrayAccess { $str.=($str?',':''). var_export($key,TRUE).'=>'. $this->stringify($val, - array_merge($stack,array($arg))); - return get_class($arg).'::__set_state(array('.$str.'))'; + array_merge($stack,[$arg])); + return get_class($arg).'::__set_state(['.$str.'])'; case 'array': $str=''; $num=isset($arg[0]) && @@ -627,9 +681,8 @@ final class Base extends Prefab implements ArrayAccess { foreach ($arg as $key=>$val) $str.=($str?',':''). ($num?'':(var_export($key,TRUE).'=>')). - $this->stringify($val, - array_merge($stack,array($arg))); - return 'array('.$str.')'; + $this->stringify($val,array_merge($stack,[$arg])); + return '['.$str.']'; default: return var_export($arg,TRUE); } @@ -642,7 +695,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function csv(array $args) { return implode(',',array_map('stripcslashes', - array_map(array($this,'stringify'),$args))); + array_map([$this,'stringify'],$args))); } /** @@ -666,7 +719,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $str string **/ function snakecase($str) { - return strtolower(preg_replace('/[[:upper:]]/','_\0',$str)); + return strtolower(preg_replace('/(?!^)[[:upper:]]/','_\0',$str)); } /** @@ -680,14 +733,15 @@ final class Base extends Prefab implements ArrayAccess { } /** - * Extract values of an associative array whose keys start with the given prefix + * Extract values of 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=[]; + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) + as $key) $out[substr($key,strlen($prefix))]=$arr[$key]; return $out; } @@ -749,33 +803,29 @@ final class Base extends Prefab implements ArrayAccess { * @param $func callback * @param $stack array **/ - function recursive($arg,$func,$stack=NULL) { + function recursive($arg,$func,$stack=[]) { if ($stack) { foreach ($stack as $node) if ($arg===$node) return $arg; } - else - $stack=array(); switch (gettype($arg)) { case 'object': - if (method_exists('ReflectionClass','iscloneable')) { - $ref=new ReflectionClass($arg); - if ($ref->iscloneable()) { - $arg=clone($arg); - $cast=is_a($arg,'IteratorAggregate')? - iterator_to_array($arg):get_object_vars($arg); - foreach ($cast as $key=>$val) - $arg->$key=$this->recursive( - $val,$func,array_merge($stack,array($arg))); - } + $ref=new ReflectionClass($arg); + if ($ref->iscloneable()) { + $arg=clone($arg); + $cast=is_a($arg,'IteratorAggregate')? + iterator_to_array($arg):get_object_vars($arg); + foreach ($cast as $key=>$val) + $arg->$key=$this->recursive( + $val,$func,array_merge($stack,[$arg])); } return $arg; case 'array': - $copy=array(); + $copy=[]; foreach ($arg as $key=>$val) $copy[$key]=$this->recursive($val,$func, - array_merge($stack,array($arg))); + array_merge($stack,[$arg])); return $copy; } return $func($arg); @@ -822,20 +872,23 @@ final class Base extends Prefab implements ArrayAccess { $conv=localeconv(); return preg_replace_callback( '/\{(?P\d+)\s*(?:,\s*(?P\w+)\s*'. - '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'. + '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. '(?:,\s*(?P.+?))?)?)?\}/', function($expr) use($args,$conv) { extract($expr); extract($conv); if (!array_key_exists($pos,$args)) return $expr[0]; - if (isset($type)) + if (isset($type)) { + if (isset($this->hive['FORMATS'][$type])) + return $this->call($this->hive['FORMATS'][$type], + [$args[$pos],$mod,isset($prop)?$prop:null]); switch ($type) { case 'plural': preg_match_all('/(?\w+)'. '(?:\s*\{\s*(?.+?)\s*\})/', $mod,$matches,PREG_SET_ORDER); - $ord=array('zero','one','two'); + $ord=['zero','one','two']; foreach ($matches as $match) { extract($match); if (isset($ord[$args[$pos]]) && @@ -850,12 +903,14 @@ final class Base extends Prefab implements ArrayAccess { $args[$pos],0,'',$thousands_sep); case 'currency': $int=$cstm=false; - if (isset($prop) && $cstm=!$int=($prop=='int')) + if (isset($prop) && + $cstm=!$int=($prop=='int')) $currency_symbol=$prop; - if (!$cstm && function_exists('money_format')) + if (!$cstm && + function_exists('money_format')) return money_format( '%'.($int?'i':'n'),$args[$pos]); - $fmt=array( + $fmt=[ 0=>'(nc)',1=>'(n c)', 2=>'(nc)',10=>'+nc', 11=>'+n c',12=>'+ nc', @@ -871,7 +926,7 @@ final class Base extends Prefab implements ArrayAccess { 130=>'+cn',131=>'+c n', 132=>'+ cn',140=>'c+n', 141=>'c+ n',142=>'c +n' - ); + ]; if ($args[$pos]<0) { $sgn=$negative_sign; $pre='n'; @@ -881,14 +936,14 @@ final class Base extends Prefab implements ArrayAccess { $pre='p'; } return str_replace( - array('+','n','c'), - array($sgn,number_format( + ['+','n','c'], + [$sgn,number_format( abs($args[$pos]), $frac_digits, $decimal_point, $thousands_sep), $int?$int_curr_symbol - :$currency_symbol), + :$currency_symbol], $fmt[(int)( (${$pre.'_cs_precedes'}%2). (${$pre.'_sign_posn'}%5). @@ -899,12 +954,10 @@ final class Base extends Prefab implements ArrayAccess { return number_format( $args[$pos]*100,0,$decimal_point, $thousands_sep).'%'; - case 'decimal': - return number_format( - $args[$pos],isset($prop)?$prop:2, - $decimal_point,$thousands_sep); } - break; + return number_format( + $args[$pos],isset($prop)?$prop:2, + $decimal_point,$thousands_sep); case 'date': if (empty($mod) || $mod=='short') $prop='%x'; @@ -918,6 +971,7 @@ final class Base extends Prefab implements ArrayAccess { default: return $expr[0]; } + } return $args[$pos]; }, $val @@ -932,8 +986,8 @@ final class Base extends Prefab implements ArrayAccess { function language($code) { $code=preg_replace('/\h+|;q=[0-9.]+/','',$code); $code.=($code?',':'').$this->fallback; - $this->languages=array(); - foreach (array_reverse(explode(',',$code)) as $lang) { + $this->languages=[]; + foreach (array_reverse(explode(',',$code)) as $lang) if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) { // Generic language array_unshift($this->languages,$parts[1]); @@ -943,11 +997,11 @@ final class Base extends Prefab implements ArrayAccess { array_unshift($this->languages,$parts[0]); } } - } $this->languages=array_unique($this->languages); - $locales=array(); + $locales=[]; $windows=preg_match('/^win/i',PHP_OS); - foreach ($this->languages as $locale) { + // Work around PHP's Turkish locale bug + foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) { if ($windows) { $parts=explode('-',$locale); $locale=@constant('ISO::LC_'.$parts[0]); @@ -966,10 +1020,16 @@ final class Base extends Prefab implements ArrayAccess { * Return lexicon entries * @return array * @param $path string + * @param $ttl int **/ - function lexicon($path) { - $lex=array(); - foreach ($this->languages?:explode(',',$this->fallback) as $lang) + function lexicon($path,$ttl=0) { + $languages=$this->languages?:explode(',',$this->fallback); + $cache=Cache::instance(); + if ($cache->exists( + $hash=$this->hash(implode(',',$languages)).'.dic',$lex)) + return $lex; + $lex=[]; + foreach ($languages as $lang) foreach ($this->split($path) as $dir) if ((is_file($file=($base=$dir.$lang).'.php') || is_file($file=$base.'.php')) && @@ -994,6 +1054,8 @@ final class Base extends Prefab implements ArrayAccess { '/\\\\\h*\r?\n/','',$match['rval'])); } } + if ($ttl) + $cache->set($hash,$lex,$ttl); return $lex; } @@ -1032,7 +1094,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function status($code) { $reason=@constant('self::HTTP_'.$code); - if (PHP_SAPI!='cli' && !headers_sent()) + if (!$this->hive['CLI'] && !headers_sent()) header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); return $reason; } @@ -1043,20 +1105,25 @@ final class Base extends Prefab implements ArrayAccess { * @param $secs int **/ function expire($secs=0) { - if (PHP_SAPI!='cli') { - header('X-Content-Type-Options: nosniff'); - header('X-Frame-Options: '.$this->hive['XFRAME']); - header('X-Powered-By: '.$this->hive['PACKAGE']); + if (!$this->hive['CLI'] && !headers_sent()) { + if ($this->hive['PACKAGE']) + header('X-Powered-By: '.$this->hive['PACKAGE']); + if ($this->hive['XFRAME']) + header('X-Frame-Options: '.$this->hive['XFRAME']); header('X-XSS-Protection: 1; mode=block'); - if ($secs) { + header('X-Content-Type-Options: nosniff'); + if ($this->hive['VERB']=='GET' && $secs) { $time=microtime(TRUE); header_remove('Pragma'); + header('Cache-Control: max-age='.(int)$secs); header('Expires: '.gmdate('r',$time+$secs)); - header('Cache-Control: max-age='.$secs); header('Last-Modified: '.gmdate('r')); } - else + else { + header('Pragma: no-cache'); header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Expires: '.gmdate('r',0)); + } } } @@ -1099,14 +1166,14 @@ final class Base extends Prefab implements ArrayAccess { } /** - * Return filtered, formatted stack trace + * Return filtered stack trace as a formatted string (or array) * @return string|array * @param $trace array|NULL * @param $format bool **/ - function trace(array $trace=NULL, $format=TRUE) { + function trace(array $trace=NULL,$format=TRUE) { if (!$trace) { - $trace=debug_backtrace(FALSE); + $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $frame=$trace[0]; if (isset($frame['file']) && $frame['file']==__FILE__) array_shift($trace); @@ -1150,8 +1217,9 @@ final class Base extends Prefab implements ArrayAccess { * @param $code int * @param $text string * @param $trace array + * @param $level int **/ - function error($code,$text='',array $trace=NULL) { + function error($code,$text='',array $trace=NULL,$level=0) { $prior=$this->hive['ERROR']; $header=$this->status($code); $req=$this->hive['VERB'].' '.$this->hive['PATH']; @@ -1162,22 +1230,24 @@ final class Base extends Prefab implements ArrayAccess { foreach (explode("\n",$trace) as $nexus) if ($nexus) error_log($nexus); - if ($highlight=PHP_SAPI!='cli' && !$this->hive['AJAX'] && + if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) $trace=$this->highlight($trace); - $this->hive['ERROR']=array( + $this->hive['ERROR']=[ 'status'=>$header, 'code'=>$code, 'text'=>$text, - 'trace'=>$trace - ); + 'trace'=>$trace, + 'level'=>$level + ]; + $this->expire(-1); $handler=$this->hive['ONERROR']; $this->hive['ONERROR']=NULL; $eol="\n"; if ((!$handler || - $this->call($handler,array($this,$this->hive['PARAMS']), + $this->call($handler,[$this,$this->hive['PARAMS']], 'beforeroute,afterroute')===FALSE) && - !$prior && PHP_SAPI!='cli' && !$this->hive['QUIET']) + !$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) echo $this->hive['AJAX']? json_encode($this->hive['ERROR']): (''.$eol. @@ -1208,8 +1278,8 @@ final class Base extends Prefab implements ArrayAccess { function mock($pattern, array $args=NULL,array $headers=NULL,$body=NULL) { if (!$args) - $args=array(); - $types=array('sync','ajax'); + $args=[]; + $types=['sync','ajax','cli']; preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); $verb=strtoupper($parts[1]); @@ -1218,7 +1288,7 @@ final class Base extends Prefab implements ArrayAccess { user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR); $parts[4]=$this->hive['ALIASES'][$parts[2]]; $parts[4]=$this->build($parts[4], - isset($parts[3])?$this->parse($parts[3]):array()); + isset($parts[3])?$this->parse($parts[3]):[]); } if (empty($parts[4])) user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); @@ -1226,11 +1296,12 @@ final class Base extends Prefab implements ArrayAccess { parse_str(@$url['query'],$GLOBALS['_GET']); if (preg_match('/GET|HEAD/',$verb)) $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); - $GLOBALS['_POST']=$verb=='POST'?$args:array(); + $GLOBALS['_POST']=$verb=='POST'?$args:[]; $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); - foreach ($headers?:array() as $key=>$val) + foreach ($headers?:[] as $key=>$val) $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; $this->hive['VERB']=$verb; + $this->hive['PATH']=$url['path']; $this->hive['URI']=$this->hive['BASE'].$url['path']; if ($GLOBALS['_GET']) $this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']); @@ -1239,6 +1310,8 @@ final class Base extends Prefab implements ArrayAccess { $this->hive['BODY']=$body?:http_build_query($args); $this->hive['AJAX']=isset($parts[5]) && preg_match('/ajax/i',$parts[5]); + $this->hive['CLI']=isset($parts[5]) && + preg_match('/cli/i',$parts[5]); return $this->run(); } @@ -1251,7 +1324,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $kbps int **/ function route($pattern,$handler,$ttl=0,$kbps=0) { - $types=array('sync','ajax'); + $types=['sync','ajax','cli']; $alias=null; if (is_array($pattern)) { foreach ($pattern as $item) @@ -1269,39 +1342,43 @@ final class Base extends Prefab implements ArrayAccess { } if (empty($parts[3])) user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); - $type=empty($parts[5])? - self::REQ_SYNC|self::REQ_AJAX: - constant('self::REQ_'.strtoupper($parts[5])); + $type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5])); foreach ($this->split($parts[1]) as $verb) { if (!preg_match('/'.self::VERBS.'/',$verb)) $this->error(501,$verb.' '.$this->hive['URI']); $this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]= - array($handler,$ttl,$kbps,$alias); + [$handler,$ttl,$kbps,$alias]; } } /** * Reroute to specified URI * @return NULL - * @param $url string + * @param $url array|string * @param $permanent bool **/ function reroute($url=NULL,$permanent=FALSE) { if (!$url) $url=$this->hive['REALM']; - if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*)/',$url,$parts)) { + if (is_array($url)) + $url=call_user_func_array([$this,'alias'],$url); + elseif (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*(\?.+)*)/',$url,$parts)) { if (empty($this->hive['ALIASES'][$parts[1]])) user_error(sprintf(self::E_Named,$parts[1]),E_USER_ERROR); $url=$this->hive['ALIASES'][$parts[1]]; } - $url=$this->build($url, - isset($parts[2])?$this->parse($parts[2]):array()); + $url=$this->build($url,isset($parts[2])?$this->parse($parts[2]):[]). + (isset($parts[3])?$parts[3]:''); if (($handler=$this->hive['ONREROUTE']) && - $this->call($handler,array($url,$permanent))!==FALSE) + $this->call($handler,[$url,$permanent])!==FALSE) return; - if ($url[0]=='/') - $url=$this->hive['BASE'].$url; - if (PHP_SAPI!='cli') { + if ($url[0]=='/') { + $port=$this->hive['PORT']; + $port=in_array($port,[80,443])?'':':'.$port; + $url=$this->hive['SCHEME'].'://'. + $this->hive['HOST'].$port.$this->hive['BASE'].$url; + } + if (!$this->hive['CLI']) { header('Location: '.$url); $this->status($permanent?301:302); die; @@ -1326,7 +1403,7 @@ final class Base extends Prefab implements ArrayAccess { foreach (explode('|',self::VERBS) as $method) $this->route($method.' '.$url,is_string($class)? $class.'->'.$this->hive['PREMAP'].strtolower($method): - array($class,$this->hive['PREMAP'].strtolower($method)), + [$class,$this->hive['PREMAP'].strtolower($method)], $ttl,$kbps); } @@ -1381,10 +1458,31 @@ final class Base extends Prefab implements ArrayAccess { if (!$url) $url=$this->rel($this->hive['URI']); $case=$this->hive['CASELESS']?'i':''; + $wild=preg_quote($pattern,'/'); + $i=0; + while (is_int($pos=strpos($wild,'\*'))) { + $wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2); + $i++; + } preg_match('/^'. - preg_replace('/((\\\{)?@(\w+\b)(?(2)\\\}))/','(?P<\3>[^\/\?]+)', - str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). - '\/?(?:\?.*)?$/'.$case.'um',$url,$args); + preg_replace( + '/((\\\{)?@(\w+\b)(?(2)\\\}))/', + '(?P<\3>[^\/\?]+)', + $wild).'\/?$/'.$case.'um',$url,$args); + foreach (array_keys($args) as $key) { + if (preg_match('/_\d+/',$key)) { + if (empty($args['*'])) + $args['*']=$args[$key]; + else { + if (is_string($args['*'])) + $args['*']=[$args['*']]; + array_push($args['*'],$args[$key]); + } + unset($args[$key]); + } + elseif (is_numeric($key) && $key) + unset($args[$key]); + } return $args; } @@ -1400,14 +1498,18 @@ final class Base extends Prefab implements ArrayAccess { // No routes defined user_error(self::E_Routes,E_USER_ERROR); // Match specific routes first - $paths=array(); - foreach ($keys=array_keys($this->hive['ROUTES']) as $key) - $paths[]=str_replace('@','*@',$key); + $paths=[]; + foreach ($keys=array_keys($this->hive['ROUTES']) as $key) { + $path=preg_replace('/@\w+/','*@',$key); + if (substr($path,-1)!='*') + $path.='+'; + $paths[]=$path; + } $vals=array_values($this->hive['ROUTES']); array_multisort($paths,SORT_DESC,$keys,$vals); $this->hive['ROUTES']=array_combine($keys,$vals); // Convert to BASE-relative URL - $req=$this->rel(urldecode($this->hive['URI'])); + $req=$this->rel(urldecode($this->hive['PATH'])); if ($cors=(isset($this->hive['HEADERS']['Origin']) && $this->hive['CORS']['origin'])) { $cors=$this->hive['CORS']; @@ -1415,45 +1517,42 @@ final class Base extends Prefab implements ArrayAccess { header('Access-Control-Allow-Credentials: '. ($cors['credentials']?'true':'false')); } - $allowed=array(); + $allowed=[]; foreach ($this->hive['ROUTES'] as $pattern=>$routes) { if (!$args=$this->mask($pattern,$req)) continue; ksort($args); $route=NULL; - if (isset( - $routes[$ptr=$this->hive['AJAX']+1][$this->hive['VERB']])) + $ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1; + if (isset($routes[$ptr][$this->hive['VERB']]) || + isset($routes[$ptr=0])) $route=$routes[$ptr]; - elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX])) - $route=$routes[self::REQ_SYNC|self::REQ_AJAX]; if (!$route) continue; if ($this->hive['VERB']!='OPTIONS' && isset($route[$this->hive['VERB']])) { - $parts=parse_url($req); if ($this->hive['VERB']=='GET' && - preg_match('/.+\/$/',$parts['path'])) - $this->reroute(substr($parts['path'],0,-1). - (isset($parts['query'])?('?'.$parts['query']):'')); + preg_match('/.+\/$/',$this->hive['PATH'])) + $this->reroute(substr($this->hive['PATH'],0,-1). + ($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; - if (is_bool(strpos($pattern,'/*'))) - foreach (array_keys($args) as $key) - if (is_numeric($key) && $key) - unset($args[$key]); // Capture values of route pattern tokens $this->hive['PARAMS']=$args; // Save matching route $this->hive['ALIAS']=$alias; $this->hive['PATTERN']=$pattern; if ($cors && $cors['expose']) - header('Access-Control-Expose-Headers: '.(is_array($cors['expose'])? - implode(',',$cors['expose']):$cors['expose'])); + header('Access-Control-Expose-Headers: '. + (is_array($cors['expose'])? + implode(',',$cors['expose']):$cors['expose'])); if (is_string($handler)) { // Replace route pattern tokens in handler if any $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', function($id) use($args) { $pid=count($id)>2?2:1; - return isset($args[$id[$pid]])?$args[$id[$pid]]:$id[0]; + return isset($args[$id[$pid]])? + $args[$id[$pid]]: + $id[0]; }, $handler ); @@ -1481,7 +1580,7 @@ final class Base extends Prefab implements ArrayAccess { } // Retrieve from cache backend list($headers,$body,$result)=$data; - if (PHP_SAPI!='cli') + if (!$this->hive['CLI']) array_walk($headers,'header'); $this->expire($cached[0]+$ttl-$now); } @@ -1496,15 +1595,15 @@ final class Base extends Prefab implements ArrayAccess { $this->hive['BODY']=file_get_contents('php://input'); ob_start(); // Call route handler - $result=$this->call($handler,array($this,$args), + $result=$this->call($handler,[$this,$args], 'beforeroute,afterroute'); $body=ob_get_clean(); if (isset($cache) && !error_get_last()) { // Save to cache backend - $cache->set($hash,array( + $cache->set($hash,[ // Remove cookies preg_grep('/Set-Cookie\:/',headers_list(), - PREG_GREP_INVERT),$body,$result),$ttl); + PREG_GREP_INVERT),$body,$result],$ttl); } } $this->hive['RESPONSE']=$body; @@ -1530,7 +1629,7 @@ final class Base extends Prefab implements ArrayAccess { if (!$allowed) // URL doesn't match any route $this->error(404); - elseif (PHP_SAPI!='cli') { + elseif (!$this->hive['CLI']) { // Unhandled HTTP method header('Allow: '.implode(',',array_unique($allowed))); if ($cors) { @@ -1559,9 +1658,10 @@ final class Base extends Prefab implements ArrayAccess { **/ function until($func,$args=NULL,$timeout=60) { if (!$args) - $args=array(); + $args=[]; $time=time(); - $limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1)); + $max=ini_get('max_execution_time'); + $limit=max(0,($max?min($timeout,$max):$timeout)-1); $out=''; // Turn output buffering on ob_start(); @@ -1569,15 +1669,17 @@ final class Base extends Prefab implements ArrayAccess { while ( // No error occurred !$this->hive['ERROR'] && + // Got time left? + time()-$time+1<$limit && // Still alive? !connection_aborted() && - // Got time left? - (time()-$time+1<$limit) && // Restart session - @session_start() && + !headers_sent() && + (session_status()==PHP_SESSION_ACTIVE || session_start()) && // CAUTION: Callback will kill host if it never becomes truthy! - !($out=$this->call($func,$args))) { - session_commit(); + !$out=$this->call($func,$args)) { + if (!$this->hive['CLI']) + session_commit(); // Hush down sleep(1); } @@ -1590,12 +1692,14 @@ final class Base extends Prefab implements ArrayAccess { * Disconnect HTTP client **/ function abort() { - @session_start(); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); session_commit(); $out=''; while (ob_get_level()) $out=ob_get_clean().$out; header('Content-Length: '.strlen($out)); + header('Connection: close'); echo $out; flush(); if (function_exists('fastcgi_finish_request')) @@ -1618,12 +1722,12 @@ final class Base extends Prefab implements ArrayAccess { $parts[1]=call_user_func($parts[1].'::instance'); else { $ref=new ReflectionClass($parts[1]); - $parts[1]=method_exists($parts[1],'__construct')? + $parts[1]=method_exists($parts[1],'__construct') && $args? $ref->newinstanceargs($args): $ref->newinstance(); } } - $func=array($parts[1],$parts[3]); + $func=[$parts[1],$parts[3]]; } return $func; } @@ -1637,7 +1741,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function call($func,$args=NULL,$hooks='') { if (!is_array($args)) - $args=array($args); + $args=[$args]; // Grab the real handler behind the string representation if (is_string($func)) $func=$this->grab($func,$args); @@ -1645,7 +1749,7 @@ final class Base extends Prefab implements ArrayAccess { if (!is_callable($func)) // No route handler if ($hooks=='beforeroute,afterroute') { - $allowed=array(); + $allowed=[]; if (is_array($func)) $allowed=array_intersect( array_map('strtoupper',get_class_methods($func[0])), @@ -1666,16 +1770,16 @@ final class Base extends Prefab implements ArrayAccess { // Execute pre-route hook if any if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && method_exists($func[0],$hook) && - call_user_func_array(array($func[0],$hook),$args)===FALSE) + call_user_func_array([$func[0],$hook],$args)===FALSE) return FALSE; // Execute callback - $out=call_user_func_array($func,$args?:array()); + $out=call_user_func_array($func,$args?:[]); if ($out===FALSE) return FALSE; // Execute post-route hook if any if ($obj && $hooks && in_array($hook='afterroute',$hooks) && method_exists($func[0],$hook) && - call_user_func_array(array($func[0],$hook),$args)===FALSE) + call_user_func_array([$func[0],$hook],$args)===FALSE) return FALSE; return $out; } @@ -1688,7 +1792,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $args mixed **/ function chain($funcs,$args=NULL) { - $out=array(); + $out=[]; foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) $out[]=$this->call($func,$args); return $out; @@ -1703,7 +1807,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function relay($funcs,$args=NULL) { foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) - $args=array($this->call($func,$args)); + $args=[$this->call($func,$args)]; return array_shift($args); } @@ -1725,12 +1829,17 @@ final class Base extends Prefab implements ArrayAccess { $matches,PREG_SET_ORDER); if ($matches) { $sec='globals'; + $cmd=[]; foreach ($matches as $match) { if ($match['section']) { $sec=$match['section']; - if (preg_match('/^(?!(?:global|config|route|map|redirect)s\b)'. - '((?:\.?\w)+)/i',$sec,$msec) && !$this->exists($msec[0])) + if (preg_match( + '/^(?!(?:global|config|route|map|redirect)s\b)'. + '((?:\.?\w)+)/i',$sec,$msec) && + !$this->exists($msec[0])) $this->set($msec[0],NULL); + preg_match('/^(config|route|map|redirect)s\b|'. + '^((?:\.?\w)+)\s*\>\s*(.*)/i',$sec,$cmd); } else { if ($allow) { @@ -1739,45 +1848,54 @@ final class Base extends Prefab implements ArrayAccess { $match['rval']=Preview::instance()-> resolve($match['rval']); } - if (preg_match('/^(config|route|map|redirect)s\b/i', - $sec,$cmd)) { - call_user_func_array( - array($this,$cmd[1]), - array_merge(array($match['lval']), + if (!empty($cmd)) { + (isset($cmd[3])) ? + $this->call($cmd[3],[$match['lval'],$match['rval'],$cmd[2]]) + : call_user_func_array( + [$this,$cmd[1]], + array_merge([$match['lval']], str_getcsv($match['rval']))); } else { + $rval=preg_replace( + '/\\\\\h*(\r?\n)/','\1',$match['rval']); + $ttl=NULL; + if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) { + array_shift($tmp); + list($rval,$ttl)=$tmp; + } $args=array_map( function($val) { if (is_numeric($val)) return $val+0; - $val=ltrim($val); + $val=trim($val); if (preg_match('/^\w+$/i',$val) && defined($val)) return constant($val); - return trim(preg_replace( - array('/\\\\"/','/\\\\\h*(\r?\n)/'), - array('"','\1'),$val)); + return preg_replace('/\\\\"/','"',$val); }, // Mark quoted strings with 0x00 whitespace str_getcsv(preg_replace('/(?[^:]+)(?:\:(?.+))?/', $sec,$parts); $func=isset($parts['func'])?$parts['func']:NULL; $custom=(strtolower($parts['section'])!='globals'); if ($func) - $args=array($this->call($func, - count($args)>1?array($args):$args)); + $args=[$this->call($func,$args)]; + if (count($args)>1) + $args=[$args]; + if (isset($ttl)) + $args=array_merge($args,[$ttl]); call_user_func_array( - array($this,'set'), + [$this,'set'], array_merge( - array( + [ ($custom?($parts['section'].'.'):''). $match['lval'] - ), - count($args)>1?array($args):$args + ], + $args ) ); } @@ -1799,8 +1917,7 @@ final class Base extends Prefab implements ArrayAccess { mkdir($tmp,self::MODE,TRUE); // Use filesystem lock if (is_file($lock=$tmp. - $this->hash($this->hive['ROOT'].$this->hive['BASE']).'.'. - $this->hash($id).'.lock') && + $this->get('SEED').'.'.$this->hash($id).'.lock') && filemtime($lock)+ini_get('max_execution_time')hive['UNLOAD']; if ((!$handler || $this->call($handler,$this)===FALSE) && $error && in_array($error['type'], - array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR))) + [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) // Fatal error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($error)); + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); } /** @@ -2018,37 +2135,73 @@ final class Base extends Prefab implements ArrayAccess { @ini_set('magic_quotes_gpc',0); @ini_set('register_globals',0); // Intercept errors/exceptions; PHP5.3-compatible - error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); + $check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); $fw=$this; set_exception_handler( function($obj) use($fw) { $fw->hive['EXCEPTION']=$obj; - $fw->error(500,$obj->getmessage(),$obj->gettrace()); + $fw->error(500, + $obj->getmessage().' '. + '['.$obj->getFile().':'.$obj->getLine().']', + $obj->gettrace()); } ); set_error_handler( - function($code,$text) use($fw) { - if ($code & error_reporting()) - $fw->error(500,$text); + function($level,$text) use($fw) { + if ($level & error_reporting()) + $fw->error(500,$text,NULL,$level); } ); if (!isset($_SERVER['SERVER_NAME'])) $_SERVER['SERVER_NAME']=gethostname(); - if (PHP_SAPI=='cli') { + if ($cli=PHP_SAPI=='cli') { // Emulate HTTP request - if (isset($_SERVER['argc']) && $_SERVER['argc']<2) { + $_SERVER['REQUEST_METHOD']='GET'; + if (!isset($_SERVER['argv'][1])) { $_SERVER['argc']++; $_SERVER['argv'][1]='/'; } - $_SERVER['REQUEST_METHOD']='GET'; - $_SERVER['REQUEST_URI']=$_SERVER['argv'][1]; + if (substr($_SERVER['argv'][1],0,1)=='/') + $_SERVER['REQUEST_URI']=$_SERVER['argv'][1]; + else { + $req=$opts=''; + foreach($_SERVER['argv'] as $i=>$arg) { + if (!$i) continue; + if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) { + foreach($m[1]?[$m[2]]:str_split($m[2]) as $k) + $opts.=($opts?'&':'').$k.'='; + if (isset($m[3])) + $opts.=$m[3]; + } else + $req.='/'.$arg; + } + $_SERVER['REQUEST_URI']=($req?:'/').'?'.urlencode($opts); + parse_str($opts,$GLOBALS['_GET']); + } + } + $headers=[]; + if (!$cli) { + if (function_exists('getallheaders')) { + foreach (getallheaders() as $key=>$val) { + $tmp=strtoupper(strtr($key,'-','_')); + // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ + $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); + $headers[$key]=$val; + if (isset($_SERVER['HTTP_'.$tmp])) + $headers[$key]=&$_SERVER['HTTP_'.$tmp]; + } + } + else { + if (isset($_SERVER['CONTENT_LENGTH'])) + $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; + if (isset($_SERVER['CONTENT_TYPE'])) + $headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; + foreach (array_keys($_SERVER) as $key) + if (substr($key,0,5)=='HTTP_') + $headers[strtr(ucwords(strtolower(strtr( + substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; + } } - $headers=array(); - if (PHP_SAPI!='cli') - foreach (array_keys($_SERVER) as $key) - if (substr($key,0,5)=='HTTP_') - $headers[strtr(ucwords(strtolower(strtr( - substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; if (isset($headers['X-HTTP-Method-Override'])) $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override']; elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) @@ -2057,7 +2210,7 @@ final class Base extends Prefab implements ArrayAccess { isset($headers['X-Forwarded-Proto']) && $headers['X-Forwarded-Proto']=='https'?'https':'http'; // Create hive early on to expose header methods - $this->hive=array('HEADERS'=>$headers); + $this->hive=['HEADERS'=>&$headers]; if (function_exists('apache_setenv')) { // Work around Apache pre-2.4 VirtualDocumentRoot bug $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'', @@ -2066,13 +2219,14 @@ final class Base extends Prefab implements ArrayAccess { } $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); $base=''; - if (PHP_SAPI!='cli') + if (!$cli) $base=rtrim($this->fixslashes( dirname($_SERVER['SCRIPT_NAME'])),'/'); $uri=parse_url($_SERVER['REQUEST_URI']); $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); + session_cache_limiter(''); call_user_func_array('session_set_cookie_params', - $jar=array( + $jar=[ 'expire'=>0, 'path'=>$base?:'/', 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && @@ -2080,43 +2234,45 @@ final class Base extends Prefab implements ArrayAccess { $_SERVER['SERVER_NAME']:'', 'secure'=>($scheme=='https'), 'httponly'=>TRUE - ) + ] ); $port=0; if (isset($_SERVER['SERVER_PORT'])) $port=$_SERVER['SERVER_PORT']; // Default configuration - $this->hive+=array( + $this->hive+=[ 'AGENT'=>$this->agent(), 'AJAX'=>$this->ajax(), 'ALIAS'=>NULL, - 'ALIASES'=>array(), + 'ALIASES'=>[], 'AUTOLOAD'=>'./', 'BASE'=>$base, 'BITMASK'=>ENT_COMPAT, 'BODY'=>NULL, 'CACHE'=>FALSE, 'CASELESS'=>TRUE, - 'CONFIG'=>NULL, - 'CORS'=>array( + 'CLI'=>$cli, + 'CORS'=>[ 'headers'=>'', 'origin'=>FALSE, 'credentials'=>FALSE, 'expose'=>FALSE, - 'ttl'=>0), + 'ttl'=>0 + ], 'DEBUG'=>0, - 'DIACRITICS'=>array(), + 'DIACRITICS'=>[], 'DNSBL'=>'', - 'EMOJI'=>array(), + 'EMOJI'=>[], 'ENCODING'=>$charset, 'ERROR'=>NULL, 'ESCAPE'=>TRUE, 'EXCEPTION'=>NULL, 'EXEMPT'=>NULL, 'FALLBACK'=>$this->fallback, + 'FORMATS'=>[], 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', 'HALT'=>TRUE, - 'HIGHLIGHT'=>TRUE, + 'HIGHLIGHT'=>FALSE, 'HOST'=>$_SERVER['SERVER_NAME'], 'IP'=>$this->ip(), 'JAR'=>$jar, @@ -2128,7 +2284,7 @@ final class Base extends Prefab implements ArrayAccess { 'ONERROR'=>NULL, 'ONREROUTE'=>NULL, 'PACKAGE'=>self::PACKAGE, - 'PARAMS'=>array(), + 'PARAMS'=>[], 'PATH'=>$path, 'PATTERN'=>NULL, 'PLUGINS'=>$this->fixslashes(__DIR__).'/', @@ -2143,11 +2299,12 @@ final class Base extends Prefab implements ArrayAccess { (':'.$port):'').$_SERVER['REQUEST_URI'], 'RESPONSE'=>'', 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], - 'ROUTES'=>array(), + 'ROUTES'=>[], 'SCHEME'=>$scheme, + 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', 'TEMP'=>'tmp/', - 'TIME'=>microtime(TRUE), + 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], 'TZ'=>@date_default_timezone_get(), 'UI'=>'./', 'UNLOAD'=>NULL, @@ -2156,30 +2313,30 @@ final class Base extends Prefab implements ArrayAccess { 'VERB'=>&$_SERVER['REQUEST_METHOD'], 'VERSION'=>self::VERSION, 'XFRAME'=>'SAMEORIGIN' - ); + ]; if (PHP_SAPI=='cli-server' && preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) $this->reroute('/'); if (ini_get('auto_globals_jit')) // Override setting - $GLOBALS+=array('_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST); + $GLOBALS+=['_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST]; // Sync PHP globals with corresponding hive keys $this->init=$this->hive; foreach (explode('|',self::GLOBALS) as $global) { $sync=$this->sync($global); - $this->init+=array( - $global=>preg_match('/SERVER|ENV/',$global)?$sync:array() - ); + $this->init+=[ + $global=>preg_match('/SERVER|ENV/',$global)?$sync:[] + ]; } - if ($error=error_get_last()) + if ($check && $error=error_get_last()) // Error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($error)); + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); date_default_timezone_set($this->hive['TZ']); // Register framework autoloader - spl_autoload_register(array($this,'autoload')); + spl_autoload_register([$this,'autoload']); // Register shutdown handler - register_shutdown_function(array($this,'unload'),getcwd()); + register_shutdown_function([$this,'unload'],getcwd()); } } @@ -2231,7 +2388,7 @@ class Cache extends Prefab { if (!empty($raw)) { list($val,$time,$ttl)=(array)$fw->unserialize($raw); if ($ttl===0 || $time+$ttl>microtime(TRUE)) - return array($time,$ttl); + return [$time,$ttl]; $val=null; $this->clear($key); } @@ -2253,14 +2410,14 @@ class Cache extends Prefab { $time=microtime(TRUE); if ($cached=$this->exists($key)) list($time,$ttl)=$cached; - $data=$fw->serialize(array($val,$time,$ttl)); + $data=$fw->serialize([$val,$time,$ttl]); $parts=explode('=',$this->dsn,2); switch ($parts[0]) { case 'apc': case 'apcu': return apc_store($ndx,$data,$ttl); case 'redis': - return $this->ref->set($ndx,$data,array('ex'=>$ttl)); + return $this->ref->set($ndx,$data, $ttl ? ['ex'=>$ttl] : []); case 'memcache': return memcache_set($this->ref,$ndx,$data,0,$ttl); case 'wincache': @@ -2327,7 +2484,8 @@ class Cache extends Prefab { case 'apcu': $info=apc_cache_info('user'); if (!empty($info['cache_list'])) { - $key=array_key_exists('info',$info['cache_list'][0])?'info':'key'; + $key=array_key_exists('info', + $info['cache_list'][0])?'info':'key'; $mtkey=array_key_exists('mtime',$info['cache_list'][0])? 'mtime':'modification_time'; foreach ($info['cache_list'] as $item) @@ -2346,6 +2504,7 @@ class Cache extends Prefab { } return TRUE; case 'memcache': + $fw=Base::instance(); foreach (memcache_get_extended_stats( $this->ref,'slabs') as $slabs) foreach (array_filter(array_keys($slabs),'is_numeric') @@ -2353,8 +2512,9 @@ class Cache extends Prefab { foreach (memcache_get_extended_stats( $this->ref,'cachedump',$id) as $data) if (is_array($data)) - foreach ($data as $key=>$val) + foreach (array_keys($data) as $key) if (preg_match($regex,$key) && + ($val=$fw->unserialize(memcache_get($this->ref,$key))) && $val[1]+$lifetimeref,$key); return TRUE; @@ -2366,7 +2526,8 @@ class Cache extends Prefab { wincache_ucache_delete($item['key_name']); return TRUE; case 'xcache': - return TRUE; /* Not supported */ + xcache_unset_by_prefix($this->prefix.'.'); + return TRUE; case 'folder': if ($glob=@glob($parts[1].'*')) foreach ($glob as $file) @@ -2388,25 +2549,17 @@ class Cache extends Prefab { if ($dsn=trim($dsn)) { if (preg_match('/^redis=(.+)/',$dsn,$parts) && extension_loaded('redis')) { - $port=6379; - $parts=explode(':',$parts[1],2); - if (count($parts)>1) - list($host,$port)=$parts; - else - $host=$parts[0]; + list($host,$port,$db)=explode(':',$parts[1])+[1=>6379,2=>NULL]; $this->ref=new Redis; if(!$this->ref->connect($host,$port,2)) $this->ref=NULL; + if(isset($db)) + $this->ref->select($db); } elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && extension_loaded('memcache')) foreach ($fw->split($parts[1]) as $server) { - $port=11211; - $parts=explode(':',$server,2); - if (count($parts)>1) - list($host,$port)=$parts; - else - $host=$parts[0]; + list($host,$port)=explode(':',$server)+[1=>11211]; if (empty($this->ref)) $this->ref=@memcache_connect($host,$port)?:NULL; else @@ -2423,7 +2576,7 @@ class Cache extends Prefab { !is_dir($parts[1])) mkdir($parts[1],Base::MODE,TRUE); } - $this->prefix=$fw->hash($_SERVER['SERVER_NAME'].$fw->get('BASE')); + $this->prefix=$fw->get('SEED'); return $this->dsn=$dsn; } @@ -2486,9 +2639,9 @@ class View extends Prefab { protected function sandbox(array $hive=NULL) { $this->level++; $fw=Base::instance(); - $implicit=false; - if ($hive === null) { - $implicit=true; + $implicit=FALSE; + if (is_null($hive)) { + $implicit=TRUE; $hive=$fw->hive(); } if ($this->level<2 || $implicit) { @@ -2497,7 +2650,7 @@ class View extends Prefab { if (isset($hive['ALIASES'])) $hive['ALIASES']=$fw->build($hive['ALIASES']); } - unset($fw, $implicit); + unset($fw,$implicit); extract($hive); unset($hive); ob_start(); @@ -2521,10 +2674,11 @@ class View extends Prefab { return $data; foreach ($fw->split($fw->get('UI').';./') as $dir) if (is_file($this->view=$fw->fixslashes($dir.$file))) { - if (isset($_COOKIE[session_name()])) - @session_start(); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) + if ($mime && !$fw->get('CLI') && !headers_sent()) header('Content-Type: '.$mime.'; '. 'charset='.$fw->get('ENCODING')); $data=$this->sandbox($hive); @@ -2555,12 +2709,12 @@ class Preview extends View { //! MIME type $mime, //! token filter - $filter=array( + $filter=[ 'esc'=>'$this->esc', 'raw'=>'$this->raw', 'alias'=>'\Base::instance()->alias', 'format'=>'\Base::instance()->format' - ); + ]; /** * Convert token to variable @@ -2574,9 +2728,10 @@ class Preview extends View { $str,$parts)) { $str=trim($parts[1]); foreach (Base::instance()->split($parts[2]) as $func) - $str=is_string($cmd=$this->filter($func))?$cmd.'('.$str.')': + $str=is_string($cmd=$this->filter($func))? + $cmd.'('.$str.')': '\Base::instance()->call('. - '$this->filter(\''.$func.'\'),array('.$str.'))'; + '$this->filter(\''.$func.'\'),['.$str.'])'; } return $str; } @@ -2646,9 +2801,13 @@ class Preview extends View { * @param $hive array * @param $ttl int **/ - function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { + function render($file,$mime=NULL,array $hive=NULL,$ttl=0) { $fw=Base::instance(); $cache=Cache::instance(); + if ($mime) + $this->mime=$mime; + elseif (!$this->mime) + $this->mime='text/html'; if (!is_dir($tmp=$fw->get('TEMP'))) mkdir($tmp,Base::MODE,TRUE); foreach ($fw->split($fw->get('UI')) as $dir) { @@ -2656,23 +2815,23 @@ class Preview extends View { return $data; if (is_file($view=$fw->fixslashes($dir.$file))) { if (!is_file($this->view=($tmp. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($view).'.php')) || + $fw->get('SEED').'.'.$fw->hash($view).'.php')) || filemtime($this->view)\h*'. - '(?!["\'])|\{\*.+?\*\}/is','', + '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*'. + '|\{\*.+?\*\}/is','', $fw->read($view)); if (method_exists($this,'parse')) $text=$this->parse($text); $fw->write($this->view,$this->build($text)); } - if (isset($_COOKIE[session_name()])) - @session_start(); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) - header('Content-Type: '.($this->mime=$mime).'; '. + if (!$fw->get('CLI') && !headers_sent()) + header('Content-Type: '.$this->mime.'; '. 'charset='.$fw->get('ENCODING')); $data=$this->sandbox($hive); if(isset($this->trigger['afterrender'])) diff --git a/app/lib/basket.php b/app/lib/basket.php index 94e03063..ffb498e7 100644 --- a/app/lib/basket.php +++ b/app/lib/basket.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -34,7 +34,7 @@ class Basket extends Magic { //! Current item identifier $id, //! Current item contents - $item=array(); + $item=[]; /** * Return TRUE if field is defined @@ -86,7 +86,7 @@ class Basket extends Magic { * @param $val mixed **/ function find($key=NULL,$val=NULL) { - $out=array(); + $out=[]; if (isset($_SESSION[$this->key])) { foreach ($_SESSION[$this->key] as $id=>$item) if (!isset($key) || @@ -122,7 +122,7 @@ class Basket extends Magic { return $this->item=$found[0]->item; } $this->reset(); - return array(); + return []; } /** @@ -175,7 +175,7 @@ class Basket extends Magic { **/ function reset() { $this->id=NULL; - $this->item=array(); + $this->item=[]; } /** @@ -219,7 +219,7 @@ class Basket extends Magic { unset($_SESSION[$this->key]); return $out; } - return array(); + return []; } /** @@ -229,7 +229,8 @@ class Basket extends Magic { **/ function __construct($key='basket') { $this->key=$key; - @session_start(); + if (session_status()!=PHP_SESSION_ACTIVE) + session_start(); Base::instance()->sync('SESSION'); $this->reset(); } diff --git a/app/lib/bcrypt.php b/app/lib/bcrypt.php index 6ecd61e4..2f44d39b 100644 --- a/app/lib/bcrypt.php +++ b/app/lib/bcrypt.php @@ -1,26 +1,27 @@ . +* +* @deprecated use http://php.net/manual/en/ref.password.php instead (PHP 5.5+ only) +**/ - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. - - This file is part of the Fat-Free Framework (http://fatfreeframework.com). - - This is free software: you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation, either version 3 of the License, or later. - - Fat-Free Framework is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License along - with Fat-Free Framework. If not, see . - -*/ - -//! Lightweight password hashing library class Bcrypt extends Prefab { //@{ Error messages diff --git a/app/lib/cli/ws.php b/app/lib/cli/ws.php new file mode 100644 index 00000000..390fa733 --- /dev/null +++ b/app/lib/cli/ws.php @@ -0,0 +1,516 @@ +. + +*/ + +namespace CLI; + +//! RFC6455 WebSocket server +class WS { + + const + //! UUID magic string + Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + //! Max packet size + Packet=65536; + + //@{ Mask bits for first byte of header + const + Text=0x01, + Binary=0x02, + Close=0x08, + Ping=0x09, + Pong=0x0a, + OpCode=0x0f, + Finale=0x80; + //@} + + //@{ Mask bits for second byte of header + const + Length=0x7f; + //@} + + protected + $addr, + $ctx, + $wait, + $sockets, + $agents=[], + $events=[]; + + /** + * Allocate stream socket + * @return NULL + * @param $socket resource + **/ + function alloc($socket) { + if (is_bool($str=$this->read($socket))) { + $this->close($socket); + return; + } + // Get WebSocket headers + $hdrs=[]; + $CRLF="\r\n"; + $verb=NULL; + $uri=NULL; + foreach (explode($CRLF,trim($str)) as $line) + if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/', + trim($line),$match)) { + $verb=$match[1]; + $uri=$match[2]; + } + else + if (preg_match('/^(.+): (.+)/',trim($line),$match)) + // Standardize header + $hdrs[ + strtr( + ucwords( + strtolower( + strtr($match[1],'-',' ') + ) + ),' ','-' + ) + ]=$match[2]; + else { + $this->close($socket); + return; + } + if (empty($hdrs['Upgrade']) && + empty($hdrs['Sec-Websocket-Key'])) { + // Not a WebSocket request + if ($verb && $uri) + $this->write( + $socket, + $str='HTTP/1.1 400 Bad Request'.$CRLF. + 'Connection: close'.$CRLF.$CRLF + ); + $this->close($socket); + return; + } + // Handshake + $bytes=$this->write( + $socket, + $str='HTTP/1.1 101 Switching Protocols'.$CRLF. + 'Upgrade: websocket'.$CRLF. + 'Connection: Upgrade'.$CRLF. + 'Sec-WebSocket-Accept: '. + base64_encode( + sha1( + $hdrs['Sec-Websocket-Key']. + self::Magic, + TRUE + ) + ).$CRLF.$CRLF + ); + if ($bytes) { + // Connect agent to server + $this->sockets[]=$socket; + $this->agents[(int)$socket]= + new Agent($this,$socket,$verb,$uri,$hdrs); + } + else + $this->close($socket); + } + + /** + * Close stream socket + * @return NULL + * @param $socket resource + **/ + function close($socket) { + stream_socket_shutdown($socket,STREAM_SHUT_WR); + @fclose($socket); + } + + /** + * Free stream socket + * @return bool + * @param $socket resource + **/ + function free($socket) { + unset($this->sockets[array_search($socket,$this->sockets)]); + unset($this->agents[(int)$socket]); + $this->close($socket); + } + + /** + * Read from stream socket + * @return string|FALSE + * @param $socket resource + **/ + function read($socket) { + if (is_string($str=@fread($socket,self::Packet)) && + strlen($str) && + strlen($str)events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + return FALSE; + } + + /** + * Write to stream socket + * @return int|FALSE + * @param $socket resource + * @param $str string + **/ + function write($socket,$str) { + for ($i=0,$bytes=0;$ievents['error']) && + is_callable($func=$this->events['error'])) + $func($this); + return FALSE; + } + return $bytes; + } + + /** + * Return socket agents + * @return array + * @param $uri string + ***/ + function agents($uri=NULL) { + return array_filter( + $this->agents, + function($val) use($uri) { + return $uri?($val->uri()==$uri):TRUE; + } + ); + } + + /** + * Return event handlers + * @return array + **/ + function events() { + return $this->events; + } + + /** + * Bind function to event handler + * @return object + * @param $event string + * @param $func callable + **/ + function on($event,$func) { + $this->events[$event]=$func; + return $this; + } + + /** + * Terminate server + * @return NULL + * @param $signal int + **/ + function kill($signal) { + die; + } + + /** + * Execute the server process + * @return object + **/ + function run() { + $fw=\Base::instance(); + // Assign signal handlers + declare(ticks=1); + pcntl_signal(SIGINT,[$this,'kill']); + pcntl_signal(SIGTERM,[$this,'kill']); + gc_enable(); + // Activate WebSocket listener + $listen=stream_socket_server( + $this->addr,$errno,$errstr, + STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, + $this->ctx + ); + $socket=socket_import_stream($listen); + register_shutdown_function(function() use($listen) { + foreach ($this->sockets as $socket) + if ($socket!=$listen) + $this->free($socket); + $this->close($listen); + if (isset($this->events['stop']) && + is_callable($func=$this->events['stop'])) + $func($this); + }); + if ($errstr) + user_error($errstr,E_USER_ERROR); + if (isset($this->events['start']) && + is_callable($func=$this->events['start'])) + $func($this); + $this->sockets=[$listen]; + $empty=[]; + $wait=$this->wait; + while (TRUE) { + $active=$this->sockets; + $mark=microtime(TRUE); + $count=@stream_select( + $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) + ); + if (is_bool($count) && $wait) { + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + die; + } + if ($count) { + // Process active connections + foreach ($active as $socket) { + if (!is_resource($socket)) + continue; + if ($socket==$listen) { + if ($socket=@stream_socket_accept($listen,0)) + $this->alloc($socket); + else + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + } + else { + $id=(int)$socket; + if (isset($this->agents[$id]) && + $raw=$this->agents[$id]->fetch()) { + list($op,$data)=$raw; + // Dispatch + switch ($op & self::OpCode) { + case self::Ping: + $this->agents[$id]->send(self::Pong); + break; + case self::Close: + $this->free($socket); + break; + case self::Text: + $data=trim($data); + case self::Binary: + if (isset($this->events['receive']) && + is_callable($func=$this->events['receive'])) + $func($this->agents[$id],$op,$data); + break; + } + } + } + } + $wait-=microtime(TRUE)-$mark; + while ($wait<1e-6) { + $wait+=$this->wait; + $count=0; + } + } + if (!$count) { + $mark=microtime(TRUE); + foreach ($this->sockets as $socket) { + if (!is_resource($socket)) + continue; + $id=(int)$socket; + if ($socket!=$listen && + isset($this->agents[$id]) && + isset($this->events['idle']) && + is_callable($func=$this->events['idle'])) + $func($this->agents[$id]); + } + $wait=$this->wait-microtime(TRUE)+$mark; + } + gc_collect_cycles(); + } + } + + /** + * Instantiate object + * @return object + * @param $addr string + * @param $ctx resource + * @param $wait int + **/ + function __construct($addr,$ctx=NULL,$wait=60) { + $this->addr=$addr; + $this->ctx=$ctx?:stream_context_create(); + $this->wait=$wait; + $this->events=[]; + } + +} + +//! RFC6455 remote socket +class Agent { + + protected + $server, + $id, + $socket, + $flag, + $verb, + $uri, + $headers, + $events, + $buffer; + + /** + * Return server instance + * @return object + **/ + function server() { + return $this->server; + } + + /** + * Return socket ID + * @return string + **/ + function id() { + return $this->id; + } + + /** + * Return request method + * @return string + **/ + function verb() { + return $this->verb; + } + + /** + * Return request URI + * @return string + **/ + function uri() { + return $this->uri; + } + + /** + * Return socket headers + * @return string + **/ + function headers() { + return $this->headers; + } + + /** + * Frame and transmit payload + * @return string|FALSE + * @param $socket resource + * @param $op int + * @param $payload string + **/ + function send($op,$data='') { + $mask=WS::Finale | $op & WS::OpCode; + $len=strlen($data); + $str=''; + if ($len>0xffff) + $str=pack('CCNN',$mask,0x7f,$len); + else + if ($len>0x7d) + $str=pack('CCn',$mask,0x7e,$len); + else + $str=pack('CC',$mask,$len); + $str.=$data; + $server=$this->server(); + if (is_bool($server->write($this->socket,$str))) { + $this->free(); + return FALSE; + } + if (!in_array($op,[WS::Pong,WS::Close]) && + isset($this->events['send']) && + is_callable($func=$this->events['send'])) + $func($this,$op,$data); + return $data; + } + + /** + * Retrieve and unmask payload + * @return array|FALSE + **/ + function fetch() { + // Unmask payload + $server=$this->server(); + if (is_bool($buf=$server->read($this->socket))) { + $this->free(); + return FALSE; + } + $buf=($this->buffer.=$buf); + $op=ord($buf[0]) & WS::OpCode; + $len=ord($buf[1]) & WS::Length; + $pos=2; + if ($len==0x7e) { + $len=ord($buf[2])*256+ord($buf[3]); + $pos+=2; + } + else + if ($len==0x7f) { + for ($i=0,$len=0;$i<8;$i++) + $len=$len*256+ord($buf[$i+2]); + $pos+=8; + } + for ($i=0,$mask=[];$i<4;$i++) + $mask[$i]=ord($buf[$pos+$i]); + $pos+=4; + if (strlen($buf)<$len+$pos) + return FALSE; + for ($i=0,$data='';$i<$len;$i++) + $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); + $this->buffer=''; + return [$op,$data]; + } + + /** + * Free stream socket + * @return NULL + **/ + function free() { + $this->server->free($this->socket); + } + + /** + * Destroy object + * @return NULL + **/ + function __destruct() { + if (isset($this->events['disconnect']) && + is_callable($func=$this->events['disconnect'])) + $func($this); + } + + /** + * Instantiate object + * @return object + * @param $server object + * @param $socket resource + * @param $verb string + * @param $uri string + * @param $hdrs array + **/ + function __construct($server,$socket,$verb,$uri,array $hdrs) { + $this->server=$server; + $this->id=stream_socket_get_name($socket,TRUE); + $this->socket=$socket; + $this->verb=$verb; + $this->uri=$uri; + $this->headers=$hdrs; + $this->events=$server->events(); + $this->buffer=''; + if (isset($this->events['connect']) && + is_callable($func=$this->events['connect'])) + $func($this); + } + +} diff --git a/app/lib/db/cursor.php b/app/lib/db/cursor.php index baef9531..9ea7cdc9 100644 --- a/app/lib/db/cursor.php +++ b/app/lib/db/cursor.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,11 +32,11 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { protected //! Query results - $query=array(), + $query=[], //! Current position $ptr=0, //! Event listeners - $trigger=array(); + $trigger=[]; /** * Return database type @@ -126,7 +126,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { **/ function findone($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); + $options=[]; // Override limit $options['limit']=1; return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; @@ -148,11 +148,11 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { $total=$this->count($filter,$ttl); $count=ceil($total/$size); $pos=max(0,min($pos,$count-1)); - return array( + return [ 'subset'=>$this->find($filter, array_merge( - $options?:array(), - array('limit'=>$size,'offset'=>$pos*$size) + $options?:[], + ['limit'=>$size,'offset'=>$pos*$size] ), $ttl ), @@ -160,7 +160,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { 'limit'=>$size, 'count'=>$count, 'pos'=>$pos<$count?$pos:0 - ); + ]; } /** @@ -378,7 +378,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { * @return NULL **/ function reset() { - $this->query=array(); + $this->query=[]; $this->ptr=0; } diff --git a/app/lib/db/jig.php b/app/lib/db/jig.php index 736735c4..d4337933 100644 --- a/app/lib/db/jig.php +++ b/app/lib/db/jig.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -51,7 +51,7 @@ class Jig { function &read($file) { if (!$this->dir || !is_file($dst=$this->dir.$file)) { if (!isset($this->data[$file])) - $this->data[$file]=array(); + $this->data[$file]=[]; return $this->data[$file]; } $fw=\Base::instance(); @@ -106,11 +106,14 @@ class Jig { } /** - * Return profiler results + * Return 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; } /** @@ -129,12 +132,16 @@ class Jig { **/ function drop() { if (!$this->dir) - $this->data=array(); + $this->data=[]; elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) foreach ($glob as $file) @unlink($file); } + //! Prohibit cloning + private function __clone() { + } + /** * Instantiate class * @param $dir string diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php index f0f3953c..74335fcd 100644 --- a/app/lib/db/jig/mapper.php +++ b/app/lib/db/jig/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -33,7 +33,7 @@ class Mapper extends \DB\Cursor { //! Document identifier $id, //! Document contents - $document=array(); + $document=[]; /** * Return database type @@ -97,7 +97,7 @@ class Mapper extends \DB\Cursor { $mapper->id=$id; foreach ($row as $field=>$val) $mapper->document[$field]=$val; - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -111,7 +111,7 @@ class Mapper extends \DB\Cursor { function cast($obj=NULL) { if (!$obj) $obj=$this; - return $obj->document+array('_id'=>$this->id); + return $obj->document+['_id'=>$this->id]; } /** @@ -157,20 +157,20 @@ class Mapper extends \DB\Cursor { **/ function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $fw=\Base::instance(); $cache=\Cache::instance(); $db=$this->db; $now=microtime(TRUE); - $data=array(); + $data=[]; if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists( $hash=$fw->hash($this->db->dir(). - $fw->stringify(array($filter,$options))).'.jig',$data)) || + $fw->stringify([$filter,$options])).'.jig',$data)) || $cached[0]+$ttlread($this->file); if (is_null($data)) @@ -188,8 +188,8 @@ class Mapper extends \DB\Cursor { $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(); + $args=is_array($args)?$args:[1=>$args]; + $keys=$vals=[]; $tokens=array_slice( token_get_all('token($expr)),1); $data=array_filter($data, @@ -248,7 +248,7 @@ class Mapper extends \DB\Cursor { $val1[$col]=NULL; if (!array_key_exists($col,$val2)) $val2[$col]=NULL; - list($v1,$v2)=array($val1[$col],$val2[$col]); + list($v1,$v2)=[$val1[$col],$val2[$col]]; if ($out=strnatcmp($v1,$v2)* (($order==SORT_ASC)*2-1)) return $out; @@ -263,7 +263,7 @@ class Mapper extends \DB\Cursor { // Save to cache backend $cache->set($hash,$data,$ttl); } - $out=array(); + $out=[]; foreach ($data as $id=>&$doc) { unset($doc['_id']); $out[]=$this->factory($id,$doc); @@ -303,7 +303,7 @@ class Mapper extends \DB\Cursor { * @param $ofs int **/ function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:array(); + $this->document=($out=parent::skip($ofs))?$out->document:[]; $this->id=$out?$out->id:NULL; if ($this->document && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); @@ -324,10 +324,10 @@ class Mapper extends \DB\Cursor { !connection_aborted()) usleep(mt_rand(0,100)); $this->id=$id; - $pkey=array('_id'=>$this->id); + $pkey=['_id'=>$this->id]; if (isset($this->trigger['beforeinsert']) && \Base::instance()->call($this->trigger['beforeinsert'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return $this->document; $data[$id]=$this->document; $db->write($this->file,$data); @@ -335,8 +335,8 @@ class Mapper extends \DB\Cursor { $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)); + [$this,$pkey]); + $this->load(['@_id=?',$this->id]); return $this->document; } @@ -350,7 +350,7 @@ class Mapper extends \DB\Cursor { $data=&$db->read($this->file); if (isset($this->trigger['beforeupdate']) && \Base::instance()->call($this->trigger['beforeupdate'], - array($this,array('_id'=>$this->id)))===FALSE) + [$this,['_id'=>$this->id]])===FALSE) return $this->document; $data[$this->id]=$this->document; $db->write($this->file,$data); @@ -358,7 +358,7 @@ class Mapper extends \DB\Cursor { $this->file.' [update] '.json_encode($this->document)); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,array('_id'=>$this->id))); + [$this,['_id'=>$this->id]]); return $this->document; } @@ -371,7 +371,7 @@ class Mapper extends \DB\Cursor { $db=$this->db; $now=microtime(TRUE); $data=&$db->read($this->file); - $pkey=array('_id'=>$this->id); + $pkey=['_id'=>$this->id]; if ($filter) { foreach ($this->find($filter,NULL,FALSE) as $mapper) if (!$mapper->erase()) @@ -386,14 +386,14 @@ class Mapper extends \DB\Cursor { return FALSE; if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return FALSE; $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); + $args=is_array($args)?$args:[1=>$args]; foreach ($args as $key=>$val) { $vals[]=\Base::instance()-> stringify(is_array($val)?$val[0]:$val); @@ -405,7 +405,7 @@ class Mapper extends \DB\Cursor { ($filter?preg_replace($keys,$vals,$filter[0],1):'')); if (isset($this->trigger['aftererase'])) \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); + [$this,$pkey]); return TRUE; } @@ -415,7 +415,7 @@ class Mapper extends \DB\Cursor { **/ function reset() { $this->id=NULL; - $this->document=array(); + $this->document=[]; parent::reset(); } diff --git a/app/lib/db/jig/session.php b/app/lib/db/jig/session.php index e4dca2fe..d3a92d9b 100644 --- a/app/lib/db/jig/session.php +++ b/app/lib/db/jig/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('@session_id=?',$this->sid=$id)); + $this->load(['@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))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('@session_id=?',$id)); + $this->erase(['@session_id=?',$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('@stamp+?erase(['@stamp+?onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/db/mongo.php b/app/lib/db/mongo.php index ffe772e0..fedec0cd 100644 --- a/app/lib/db/mongo.php +++ b/app/lib/db/mongo.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -57,21 +57,27 @@ class Mongo { } /** - * Return MongoDB profiler results + * Return MongoDB profiler results (or disable logging) + * @param $flag bool * @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; + function log($flag=TRUE) { + if ($flag) { + $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; + } else { + $this->log=FALSE; + $this->setprofilinglevel(-1); + } return $this->log; } @@ -81,7 +87,8 @@ class Mongo { **/ function drop() { $out=$this->db->drop(); - $this->setprofilinglevel(2); + if ($this->log!==FALSE) + $this->setprofilinglevel(2); return $out; } @@ -92,7 +99,11 @@ class Mongo { * @param $args array **/ function __call($func,array $args) { - return call_user_func_array(array($this->db,$func),$args); + return call_user_func_array([$this->db,$func],$args); + } + + //! Prohibit cloning + private function __clone() { } /** @@ -104,7 +115,7 @@ class Mongo { function __construct($dsn,$dbname,array $options=NULL) { $this->uuid=\Base::instance()->hash($this->dsn=$dsn); $class=class_exists('\MongoClient')?'\MongoClient':'\Mongo'; - $this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname); + $this->db=new \MongoDB(new $class($dsn,$options?:[]),$dbname); $this->setprofilinglevel(2); } diff --git a/app/lib/db/mongo/mapper.php b/app/lib/db/mongo/mapper.php index e814b293..411c524b 100644 --- a/app/lib/db/mongo/mapper.php +++ b/app/lib/db/mongo/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -31,9 +31,11 @@ class Mapper extends \DB\Cursor { //! Mongo collection $collection, //! Mongo document - $document=array(), + $document=[], //! Mongo cursor - $cursor; + $cursor, + //! Defined fields + $fields; /** * Return database type @@ -92,7 +94,7 @@ class Mapper extends \DB\Cursor { $mapper->reset(); foreach ($row as $key=>$val) $mapper->document[$key]=$val; - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -119,48 +121,48 @@ class Mapper extends \DB\Cursor { **/ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ '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', + $fw->stringify([$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('w'=>1)); - $filter=array(); + $tmp->batchinsert($grp['retval'],['w'=>1]); + $filter=[]; $collection=$tmp; } else { - $filter=$filter?:array(); + $filter=$filter?:[]; $collection=$this->collection; } - $this->cursor=$collection->find($filter,$fields?:array()); + $this->cursor=$collection->find($filter,$fields?:[]); 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(); + $result=[]; while ($this->cursor->hasnext()) $result[]=$this->cursor->getnext(); if ($options['group']) @@ -169,7 +171,7 @@ class Mapper extends \DB\Cursor { // Save to cache backend $cache->set($hash,$result,$ttl); } - $out=array(); + $out=[]; foreach ($result as $doc) $out[]=$this->factory($doc); return $out; @@ -184,14 +186,14 @@ class Mapper extends \DB\Cursor { **/ function find($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); - return $this->select(NULL,$filter,$options,$ttl); + ]; + return $this->select($this->fields,$filter,$options,$ttl); } /** @@ -204,9 +206,9 @@ class Mapper extends \DB\Cursor { $fw=\Base::instance(); $cache=\Cache::instance(); if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( - array($filter))).'.mongo',$result)) || !$ttl || + [$filter])).'.mongo',$result)) || !$ttl || $cached[0]+$ttlcollection->count($filter?:array()); + $result=$this->collection->count($filter?:[]); if ($fw->get('CACHE') && $ttl) // Save to cache backend $cache->set($hash,$result,$ttl); @@ -221,7 +223,7 @@ class Mapper extends \DB\Cursor { * @param $ofs int **/ function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:array(); + $this->document=($out=parent::skip($ofs))?$out->document:[]; if ($this->document && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); return $out; @@ -236,13 +238,13 @@ class Mapper extends \DB\Cursor { return $this->update(); if (isset($this->trigger['beforeinsert']) && \Base::instance()->call($this->trigger['beforeinsert'], - array($this,array('_id'=>$this->document['_id'])))===FALSE) + [$this,['_id'=>$this->document['_id']]])===FALSE) return $this->document; $this->collection->insert($this->document); - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['afterinsert'])) \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkey)); + [$this,$pkey]); $this->load($pkey); return $this->document; } @@ -252,16 +254,16 @@ class Mapper extends \DB\Cursor { * @return array **/ function update() { - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['beforeupdate']) && \Base::instance()->call($this->trigger['beforeupdate'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return $this->document; $this->collection->update( - $pkey,$this->document,array('upsert'=>TRUE)); + $pkey,$this->document,['upsert'=>TRUE]); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,$pkey)); + [$this,$pkey]); return $this->document; } @@ -273,17 +275,17 @@ class Mapper extends \DB\Cursor { function erase($filter=NULL) { if ($filter) return $this->collection->remove($filter); - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return FALSE; $result=$this->collection-> - remove(array('_id'=>$this->document['_id'])); + remove(['_id'=>$this->document['_id']]); parent::erase(); if (isset($this->trigger['aftererase'])) \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); + [$this,$pkey]); return $result; } @@ -292,7 +294,7 @@ class Mapper extends \DB\Cursor { * @return NULL **/ function reset() { - $this->document=array(); + $this->document=[]; parent::reset(); } @@ -351,10 +353,12 @@ class Mapper extends \DB\Cursor { * @return void * @param $db object * @param $collection string + * @param $fields array **/ - function __construct(\DB\Mongo $db,$collection) { + function __construct(\DB\Mongo $db,$collection,$fields=NULL) { $this->db=$db; $this->collection=$db->selectcollection($collection); + $this->fields=$fields; $this->reset(); } diff --git a/app/lib/db/mongo/session.php b/app/lib/db/mongo/session.php index 0b510f13..5ea894b8 100644 --- a/app/lib/db/mongo/session.php +++ b/app/lib/db/mongo/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('session_id'=>$this->sid=$id)); + $this->load(['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))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('session_id'=>$id)); + $this->erase(['session_id'=>$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('$where'=>'this.stamp+'.$max.'<'.time())); + $this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]); return TRUE; } @@ -168,18 +169,17 @@ class Session extends Mapper { parent::__construct($db,$table); $this->onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/db/sql.php b/app/lib/db/sql.php index 60089446..48b49cc1 100644 --- a/app/lib/db/sql.php +++ b/app/lib/db/sql.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -30,6 +30,9 @@ class SQL { E_PKey='Table %s does not have a primary key'; //@} + const + PARAM_FLOAT='float'; + protected //! UUID $uuid, @@ -78,6 +81,14 @@ class SQL { return $out; } + /** + * Return transaction flag + * @return bool + **/ + function trans() { + return $this->trans; + } + /** * Map data type of argument to a PDO constant * @return int @@ -93,6 +104,8 @@ class SQL { return \PDO::PARAM_INT; case 'resource': return \PDO::PARAM_LOB; + case 'float': + return self::PARAM_FLOAT; default: return \PDO::PARAM_STR; } @@ -106,6 +119,10 @@ class SQL { **/ function value($type,$val) { switch ($type) { + case self::PARAM_FLOAT: + return (float)(is_string($val) + ? str_replace(',','.',preg_replace('/([.,])(?!\d+$)/','',$val)) + : $val); case \PDO::PARAM_NULL: return (unset)$val; case \PDO::PARAM_INT: @@ -124,15 +141,19 @@ class SQL { * @return array|int|FALSE * @param $cmds string|array * @param $args string|array - * @param $ttl int + * @param $ttl int|array * @param $log bool + * @param $stamp bool **/ - function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { + function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; $auto=FALSE; if (is_null($args)) - $args=array(); + $args=[]; elseif (is_scalar($args)) - $args=array(1=>$args); + $args=[1=>$args]; if (is_array($cmds)) { if (count($args)<($count=count($cmds))) // Apply arguments to SQL commands @@ -144,9 +165,11 @@ class SQL { } else { $count=1; - $cmds=array($cmds); - $args=array($args); + $cmds=[$cmds]; + $args=[$args]; } + if ($this->log===FALSE) + $log=FALSE; $fw=\Base::instance(); $cache=\Cache::instance(); $result=FALSE; @@ -161,10 +184,10 @@ class SQL { if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) continue; $now=microtime(TRUE); - $keys=$vals=array(); + $keys=$vals=[]; if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists( $hash=$fw->hash($this->dsn.$cmd. - $fw->stringify($arg)).'.sql',$result)) && + $fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && $cached[0]+$ttl>microtime(TRUE)) { foreach ($arg as $key=>$val) { $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); @@ -172,7 +195,7 @@ class SQL { '/'; } if ($log) - $this->log.=date('r').' ('. + $this->log.=($stamp?(date('r').' '):'').'('. sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. '[CACHED] '. preg_replace($keys,$vals, @@ -182,20 +205,22 @@ class SQL { foreach ($arg as $key=>$val) { if (is_array($val)) { // User-specified data type - $query->bindvalue($key,$val[0],$val[1]); + $query->bindvalue($key,$val[0], + $val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$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)); + ($type=$this->type($val))==self::PARAM_FLOAT? + \PDO::PARAM_STR:$type); $vals[]=$fw->stringify($this->value($type,$val)); } $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). '/'; } if ($log) - $this->log.=date('r').' ('. + $this->log.=($stamp?(date('r').' '):'').'('. sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. preg_replace($keys,$vals, str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; @@ -216,7 +241,7 @@ class SQL { if (preg_match('/sqlite2?/',$this->engine)) foreach ($result as $pos=>$rec) { unset($result[$pos]); - $result[$pos]=array(); + $result[$pos]=[]; foreach ($rec as $key=>$val) $result[$pos][trim($key,'\'"[]`')]=$val; } @@ -269,20 +294,20 @@ class SQL { * @return array|FALSE * @param $table string * @param $fields array|string - * @param $ttl int + * @param $ttl int|array **/ function schema($table,$fields=NULL,$ttl=0) { if (strpos($table,'.')) list($schema,$table)=explode('.',$table); // 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( + $cmd=[ + 'sqlite2?'=>[ + 'PRAGMA table_info("'.$table.'")', + 'name','type','dflt_value','notnull',0,'pk',TRUE], + 'mysql'=>[ + 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', + 'Field','Type','Default','Null','YES','Key','PRI'], + 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ 'SELECT '. 'c.column_name AS field,'. 'c.data_type AS type,'. @@ -309,10 +334,9 @@ class SQL { '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( + $this->quote($this->dbname)):''), + 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'], + 'oci'=>[ 'SELECT c.column_name AS field, '. 'c.data_type AS type, '. 'c.data_default AS defval, '. @@ -326,35 +350,35 @@ class SQL { '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') - ); + '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(); + $rows=[]; foreach ($this->exec($val[0],NULL,$ttl) as $row) { if (!$fields || in_array($row[$val[1]],$fields)) - $rows[$row[$val[1]]]=array( + $rows[$row[$val[1]]]=[ 'type'=>$row[$val[2]], 'pdo_type'=> preg_match('/int\b|integer/i',$row[$val[2]])? \PDO::PARAM_INT: (preg_match('/bool/i',$row[$val[2]])? \PDO::PARAM_BOOL: - (preg_match('/blob|bytea|image|binary/i', - $row[$val[2]])? - \PDO::PARAM_LOB: - \PDO::PARAM_STR)), + (preg_match( + '/blob|bytea|image|binary/i', + $row[$val[2]])?\PDO::PARAM_LOB: + (preg_match( + '/float|decimal|real|numeric|double/i', + $row[$val[2]])?self::PARAM_FLOAT: + \PDO::PARAM_STR))), 'default'=>is_string($row[$val[3]])? preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', $row[$val[3]]):$row[$val[3]], 'nullable'=>$row[$val[4]]==$val[5], 'pkey'=>$row[$val[6]]==$val[7] - ); + ]; } return $rows; } @@ -420,20 +444,22 @@ class SQL { * Return quoted identifier name * @return string * @param $key - **/ - function quotekey($key) { - $delims=array( + * @param bool $split + **/ + function quotekey($key, $split=TRUE) { + $delims=[ 'mysql'=>'``', 'sqlite2?|pgsql|oci'=>'""', 'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' - ); + ]; $use=''; foreach ($delims as $engine=>$delim) if (preg_match('/'.$engine.'/',$this->engine)) { $use=$delim; break; } - return $use[0].implode($use[1].'.'.$use[0],explode('.',$key)).$use[1]; + return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) + : $key).$use[1]; } /** @@ -443,7 +469,11 @@ class SQL { * @param $args array **/ function __call($func,array $args) { - return call_user_func_array(array($this->pdo,$func),$args); + return call_user_func_array([$this->pdo,$func],$args); + } + + //! Prohibit cloning + private function __clone() { } /** @@ -459,10 +489,10 @@ class SQL { if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) $this->dbname=$parts[1]; if (!$options) - $options=array(); + $options=[]; if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') - $options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. - strtolower(str_replace('-','',$fw->get('ENCODING'))).';'); + $options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. + strtolower(str_replace('-','',$fw->get('ENCODING'))).';']; $this->pdo=new \PDO($dsn,$user,$pw,$options); $this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); } diff --git a/app/lib/db/sql/mapper.php b/app/lib/db/sql/mapper.php index ca6f8493..ac4dd87c 100644 --- a/app/lib/db/sql/mapper.php +++ b/app/lib/db/sql/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -39,7 +39,7 @@ class Mapper extends \DB\Cursor { //! Defined fields $fields, //! Adhoc fields - $adhoc=array(); + $adhoc=[]; /** * Return database type @@ -90,7 +90,7 @@ class Mapper extends \DB\Cursor { 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 || + if ($this->fields[$key]['initial']!==$val || $this->fields[$key]['default']!==$val && is_null($val)) $this->fields[$key]['changed']=TRUE; return $this->fields[$key]['value']=$val; @@ -100,7 +100,7 @@ class Mapper extends \DB\Cursor { $this->adhoc[$key]['value']=$val; else // Parenthesize expression in case it's a subquery - $this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL); + $this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL]; return $val; } @@ -144,6 +144,8 @@ class Mapper extends \DB\Cursor { return 'bool'; case \PDO::PARAM_STR: return 'string'; + case \DB\SQL::PARAM_FLOAT: + return 'float'; } } @@ -163,10 +165,11 @@ class Mapper extends \DB\Cursor { else continue; $mapper->{$var}[$key]['value']=$val; + $mapper->{$var}[$key]['initial']=$val; if ($var=='fields' && $mapper->{$var}[$key]['pkey']) $mapper->{$var}[$key]['previous']=$val; } - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -194,37 +197,36 @@ class Mapper extends \DB\Cursor { * @param $fields string * @param $filter string|array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $db=$this->db; $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; + $args=[]; + 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:[1=>$args]; + list($filter)=$filter; } + if ($filter) + $sql.=' WHERE '.$filter; if ($options['group']) { $sql.=' GROUP BY '.implode(',',array_map( function($str) use($db) { return preg_replace_callback( '/\b(\w+)\h*(HAVING.+|$)/i', function($parts) use($db) { - return $db->quotekey($parts[1]); + return $db->quotekey($parts[1]).(isset($parts[2])?(' '.$parts[2]):''); }, $str ); @@ -243,7 +245,7 @@ class Mapper extends \DB\Cursor { } if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && ($options['limit'] || $options['offset'])) { - $pkeys=array(); + $pkeys=[]; foreach ($this->fields as $key=>$field) if ($field['pkey']) $pkeys[]=$key; @@ -274,7 +276,7 @@ class Mapper extends \DB\Cursor { $sql.=' OFFSET '.(int)$options['offset']; } $result=$this->db->exec($sql,$args,$ttl); - $out=array(); + $out=[]; foreach ($result as &$row) { foreach ($row as $field=>&$val) { if (array_key_exists($field,$this->fields)) { @@ -297,24 +299,24 @@ class Mapper extends \DB\Cursor { * @return static[] * @param $filter string|array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function find($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ '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( ($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)? $options['group']: - implode(',',array_map(array($this->db,'quotekey'), + implode(',',array_map([$this->db,'quotekey'], array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); } @@ -322,18 +324,18 @@ class Mapper extends \DB\Cursor { * Count records that match criteria * @return int * @param $filter string|array - * @param $ttl int + * @param $ttl int|array **/ function count($filter=NULL,$ttl=0) { $sql='SELECT COUNT(*) AS '. $this->db->quotekey('rows').' FROM '.$this->table; - $args=array(); + $args=[]; 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); + $args=is_array($args)?$args:[1=>$args]; list($filter)=$filter; } $sql.=' WHERE '.$filter; @@ -353,6 +355,7 @@ class Mapper extends \DB\Cursor { $dry=$this->dry(); foreach ($this->fields as $key=>&$field) { $field['value']=$dry?NULL:$out->fields[$key]['value']; + $field['initial']=$field['value']; $field['changed']=FALSE; if ($field['pkey']) $field['previous']=$dry?NULL:$out->fields[$key]['value']; @@ -372,22 +375,22 @@ class Mapper extends \DB\Cursor { * @return object **/ function insert() { - $args=array(); + $args=[]; $actr=0; $nctr=0; $fields=''; $values=''; $filter=''; - $pkeys=array(); - $nkeys=array(); - $ckeys=array(); + $pkeys=[]; + $nkeys=[]; + $ckeys=[]; $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))===FALSE) + [$this,$pkeys])===FALSE) return $this; foreach ($this->fields as $key=>&$field) { if ($field['pkey']) { @@ -396,13 +399,13 @@ class Mapper extends \DB\Cursor { empty($field['value']) && !$field['nullable']) $inc=$key; $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $nkeys[$nctr+1]=array($field['value'],$field['pdo_type']); + $nkeys[$nctr+1]=[$field['value'],$field['pdo_type']]; $nctr++; } if ($field['changed'] && $key!=$inc) { $fields.=($actr?',':'').$this->db->quotekey($key); $values.=($actr?',':'').'?'; - $args[$actr+1]=array($field['value'],$field['pdo_type']); + $args[$actr+1]=[$field['value'],$field['pdo_type']]; $actr++; $ckeys[]=$key; } @@ -420,18 +423,21 @@ class Mapper extends \DB\Cursor { $seq=NULL; if ($this->engine=='pgsql') { $names=array_keys($pkeys); - $seq=$this->source.'_'.end($names).'_seq'; + $aik=end($names); + if ($this->fields[$aik]['pdo_type']==\PDO::PARAM_INT) + $seq=$this->source.'_'.$aik.'_seq'; } - if ($this->engine!='oci') + if ($this->engine!='oci' && !($this->engine=='pgsql' && !$seq)) $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 ($inc || $filter) + $this->load($inc? + [$inc.'=?',$this->db->value( + $this->fields[$inc]['pdo_type'],$this->_id)]: + [$filter,$nkeys]); if (isset($this->trigger['afterinsert'])) \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkeys)); + [$this,$pkeys]); } return $this; } @@ -441,38 +447,42 @@ class Mapper extends \DB\Cursor { * @return object **/ function update() { - $args=array(); + $args=[]; $ctr=0; $pairs=''; $filter=''; - $pkeys=array(); + $pkeys=[]; 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))===FALSE) + [$this,$pkeys])===FALSE) return $this; 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++; + $args[++$ctr]=[$field['value'],$field['pdo_type']]; } foreach ($this->fields as $key=>$field) if ($field['pkey']) { $filter.=($filter?' AND ':' WHERE '). $this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['previous'],$field['pdo_type']); - $ctr++; + $args[++$ctr]=[$field['previous'],$field['pdo_type']]; } if ($pairs) { $sql='UPDATE '.$this->table.' SET '.$pairs.$filter; $this->db->exec($sql,$args); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,$pkeys)); + [$this,$pkeys]); } + // reset changed flag after calling afterupdate + foreach ($this->fields as $key=>&$field) { + $field['changed']=FALSE; + $field['initial']=$field['value']; + unset($field); + } return $this; } @@ -483,25 +493,25 @@ class Mapper extends \DB\Cursor { **/ function erase($filter=NULL) { if ($filter) { - $args=array(); + $args=[]; 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); + $args=is_array($args)?$args:[1=>$args]; list($filter)=$filter; } return $this->db-> exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); } - $args=array(); + $args=[]; $ctr=0; $filter=''; - $pkeys=array(); + $pkeys=[]; 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']); + $args[$ctr+1]=[$field['previous'],$field['pdo_type']]; $pkeys[$key]=$field['previous']; $ctr++; } @@ -518,13 +528,13 @@ class Mapper extends \DB\Cursor { parent::erase(); if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkeys))===FALSE) + [$this,$pkeys])===FALSE) return 0; $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)); + [$this,$pkeys]); return $out; } @@ -535,6 +545,7 @@ class Mapper extends \DB\Cursor { function reset() { foreach ($this->fields as &$field) { $field['value']=NULL; + $field['initial']=NULL; $field['changed']=FALSE; if ($field['pkey']) $field['previous']=NULL; @@ -591,7 +602,7 @@ class Mapper extends \DB\Cursor { * @param $adhoc bool **/ function fields($adhoc=TRUE) { - return array_keys($this->fields+($adhoc?$this->adhoc:array())); + return array_keys($this->fields+($adhoc?$this->adhoc:[])); } /** @@ -617,7 +628,7 @@ class Mapper extends \DB\Cursor { * @param $db object * @param $table string * @param $fields array|string - * @param $ttl int + * @param $ttl int|array **/ function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) { $this->db=$db; diff --git a/app/lib/db/sql/session.php b/app/lib/db/sql/session.php index 86be69f2..56c1f430 100644 --- a/app/lib/db/sql/session.php +++ b/app/lib/db/sql/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('session_id=?',$this->sid=$id)); + $this->load(['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))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('session_id=?',$id)); + $this->erase(['session_id=?',$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('stamp+?erase(['stamp+?name())&&$db->driver()!='pgsql')? - ($name.'.'):''))). - $table.' ('.$eol. + ($db->quotekey($name,FALSE).'.'):''))). + $db->quotekey($table,FALSE).' ('.$eol. $tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. $tab.$db->quotekey('data').' TEXT,'.$eol. $tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. - $tab.$db->quotekey('agent').' VARCHAR(255),'.$eol. + $tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. $tab.$db->quotekey('stamp').' INTEGER,'.$eol. $tab.'PRIMARY KEY ('.$db->quotekey('session_id').')'.$eol. ');' @@ -190,18 +191,17 @@ class Session extends Mapper { parent::__construct($db,$table); $this->onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/f3.php b/app/lib/f3.php index 96c7845e..d68cd1fb 100644 --- a/app/lib/f3.php +++ b/app/lib/f3.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -36,7 +36,7 @@ class F3 { static function __callstatic($func,array $args) { if (!self::$fw) self::$fw=Base::instance(); - return call_user_func_array(array(self::$fw,$func),$args); + return call_user_func_array([self::$fw,$func],$args); } } diff --git a/app/lib/image.php b/app/lib/image.php index e1acd483..a8514c22 100644 --- a/app/lib/image.php +++ b/app/lib/image.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -28,6 +28,7 @@ class Image { E_Color='Invalid color specified: %s', E_File='File not found', E_Font='CAPTCHA font not found', + E_TTF='No TrueType support in GD module', E_Length='Invalid CAPTCHA length: %s'; //@} @@ -54,9 +55,11 @@ class Image { /** * Convert RGB hex triad to array * @return array|FALSE - * @param $color int + * @param $color int|string **/ function rgb($color) { + if (is_string($color)) + $color=hexdec($color); $hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT); if (($len=strlen($hex))>6) user_error(sprintf(self::E_Color,'0x'.$hex),E_USER_ERROR); @@ -225,9 +228,17 @@ class Image { * @param $crop bool * @param $enlarge bool **/ - function resize($width,$height,$crop=TRUE,$enlarge=TRUE) { + function resize($width=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) { + if (is_null($width) && is_null($height)) + return $this; + $origw=$this->width(); + $origh=$this->height(); + if (is_null($width)) + $width=round(($height/$origh)*$origw); + if (is_null($height)) + $height=round(($width/$origw)*$origh); // Adjust dimensions; retain aspect ratio - $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); + $ratio=$origw/$origh; if (!$crop) { if ($width/$ratio<=$height) $height=round($width/$ratio); @@ -331,24 +342,24 @@ class Image { * @param $blocks int **/ function identicon($str,$size=64,$blocks=4) { - $sprites=array( - array(.5,1,1,0,1,1), - array(.5,0,1,0,.5,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.5), - array(0,.5,.5,0,1,.5,.5,1,.5,.5), - array(0,.5,1,0,1,1,0,1,1,.5), - array(1,0,1,1,.5,1,1,.5,.5,.5), - array(0,0,1,0,1,.5,0,0,.5,1,0,1), - array(0,0,.5,0,1,.5,.5,1,0,1,.5,.5), - array(.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5), - array(0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25), - array(0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1), - array(0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25), - array(0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1) - ); + $sprites=[ + [.5,1,1,0,1,1], + [.5,0,1,0,.5,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.5], + [0,.5,.5,0,1,.5,.5,1,.5,.5], + [0,.5,1,0,1,1,0,1,1,.5], + [1,0,1,1,.5,1,1,.5,.5,.5], + [0,0,1,0,1,.5,0,0,.5,1,0,1], + [0,0,.5,0,1,.5,.5,1,0,1,.5,.5], + [.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5], + [0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25], + [0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25], + [0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1] + ]; $hash=sha1($str); $this->data=imagecreatetruecolor($size,$size); list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3))); @@ -360,15 +371,10 @@ class Image { for ($i=$j,$x=$blocks-1-$j;$i<$x;$i++) { $sprite=imagecreatetruecolor($dim,$dim); imagefill($sprite,0,0,IMG_COLOR_TRANSPARENT); - if ($block=$sprites[ - hexdec($hash[($j*$blocks+$i)*2])%$ctr]) { - for ($k=0,$pts=count($block);$k<$pts;$k++) - $block[$k]*=$dim; - imagefilledpolygon($sprite,$block,$pts/2,$fg); - } - $sprite=imagerotate($sprite, - 90*(hexdec($hash[($j*$blocks+$i)*2+1])%4), - imagecolorallocatealpha($sprite,0,0,0,127)); + $block=$sprites[hexdec($hash[($j*$blocks+$i)*2])%$ctr]; + for ($k=0,$pts=count($block);$k<$pts;$k++) + $block[$k]*=$dim; + imagefilledpolygon($sprite,$block,$pts/2,$fg); for ($k=0;$k<4;$k++) { imagecopyresampled($this->data,$sprite, $i*$dim/2,$j*$dim/2,0,0,$dim/2,$dim/2,$dim,$dim); @@ -398,6 +404,10 @@ class Image { user_error(sprintf(self::E_Length,$len),E_USER_ERROR); return FALSE; } + if (!function_exists('imagettftext')) { + user_error(self::E_TTF,E_USER_ERROR); + return FALSE; + } $fw=Base::instance(); foreach ($fw->split($path?:$fw->get('UI').';./') as $dir) if (is_file($path=$dir.$font)) { @@ -405,7 +415,7 @@ class Image { $ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(), -$len)); $block=$size*3; - $tmp=array(); + $tmp=[]; for ($i=0,$width=0,$height=0;$i<$len;$i++) { // Process at 2x magnification $box=imagettfbbox($size*2,0,$path,$seed[$i]); @@ -472,8 +482,10 @@ class Image { header('Content-Type: image/'.$format); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); } - call_user_func_array('image'.$format, - array_merge(array($this->data),$args)); + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); } /** @@ -484,8 +496,10 @@ class Image { $args=func_get_args(); $format=$args?array_shift($args):'png'; ob_start(); - call_user_func_array('image'.$format, - array_merge(array($this->data),$args)); + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); return ob_get_clean(); } @@ -507,8 +521,7 @@ class Image { if (!is_dir($dir=$fw->get('TEMP'))) mkdir($dir,Base::MODE,TRUE); $this->count++; - $fw->write($dir.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->write($dir.'/'.$fw->get('SEED').'.'. $fw->hash($this->file).'-'.$this->count.'.png', $this->dump()); } @@ -523,8 +536,7 @@ class Image { function restore($state=1) { $fw=Base::instance(); if ($this->flag && is_file($file=($path=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($this->file).'-').$state.'.png')) { + $fw->get('SEED').'.'.$fw->hash($this->file).'-').$state.'.png')) { if (is_resource($this->data)) imagedestroy($this->data); $this->data=imagecreatefromstring($fw->read($file)); @@ -553,11 +565,12 @@ class Image { /** * Load string - * @return object + * @return object|FALSE * @param $str string **/ function load($str) { - $this->data=imagecreatefromstring($str); + if (!$this->data=@imagecreatefromstring($str)) + return FALSE; imagesavealpha($this->data,TRUE); $this->save(); return $this; @@ -592,9 +605,7 @@ class Image { if (is_resource($this->data)) { imagedestroy($this->data); $fw=Base::instance(); - $path=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($this->file); + $path=$fw->get('TEMP').$fw->get('SEED').'.'.$fw->hash($this->file); if ($glob=@glob($path.'*.png',GLOB_NOSORT)) foreach ($glob as $match) if (preg_match('/-(\d+)\.png/',$match)) diff --git a/app/lib/log.php b/app/lib/log.php index 6583cedb..2bbe4c27 100644 --- a/app/lib/log.php +++ b/app/lib/log.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). diff --git a/app/lib/magic.php b/app/lib/magic.php index 5669908a..76ea221e 100644 --- a/app/lib/magic.php +++ b/app/lib/magic.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -70,7 +70,7 @@ abstract class Magic implements ArrayAccess { **/ function offsetset($key,$val) { return Base::instance()->visible($this,$key)? - ($this->key=$val):$this->set($key,$val); + ($this->$key=$val):$this->set($key,$val); } /** diff --git a/app/lib/markdown.php b/app/lib/markdown.php index 9863d911..1c3fb163 100644 --- a/app/lib/markdown.php +++ b/app/lib/markdown.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -321,7 +321,7 @@ class Markdown extends Prefab { $tmp=''; while ($str!=$tmp) $str=preg_replace_callback( - '/(?special) - $this->special=array( + $this->special=[ '...'=>'…', '(tm)'=>'™', '(r)'=>'®', '(c)'=>'©' - ); + ]; foreach ($this->special as $key=>$val) $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); return htmlspecialchars($str,ENT_COMPAT, @@ -454,7 +454,7 @@ class Markdown extends Prefab { * @param $str string **/ function scan($str) { - $inline=array('img','a','text','auto','code'); + $inline=['img','a','text','auto','code']; foreach ($inline as $func) $str=$this->{'_'.$func}($str); return $str; @@ -468,7 +468,7 @@ class Markdown extends Prefab { protected function build($str) { if (!$this->blocks) { // Regexes for capturing entire blocks - $this->blocks=array( + $this->blocks=[ 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. @@ -486,7 +486,7 @@ class Markdown extends Prefab { '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' - ); + ]; } $self=$this; // Treat lines with nothing but whitespaces as empty lines @@ -546,7 +546,7 @@ class Markdown extends Prefab { if (preg_match($regex,substr($str,$ptr),$match)) { $ptr+=strlen($match[0]); $dst.=call_user_func_array( - array($this,'_'.$func), + [$this,'_'.$func], count($match)>1?array_slice($match,1):$match ); break; diff --git a/app/lib/matrix.php b/app/lib/matrix.php index 1ebce6b9..f20e9890 100644 --- a/app/lib/matrix.php +++ b/app/lib/matrix.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -45,7 +45,7 @@ class Matrix extends Prefab { * @param $var array **/ function transpose(array &$var) { - $out=array(); + $out=[]; foreach ($var as $keyx=>$cols) foreach ($cols as $keyy=>$valy) $out[$keyy][$keyx]=$valy; @@ -63,7 +63,7 @@ class Matrix extends Prefab { uasort( $var, function($val1,$val2) use($col,$order) { - list($v1,$v2)=array($val1[$col],$val2[$col]); + list($v1,$v2)=[$val1[$col],$val2[$col]]; $out=is_numeric($v1) && is_numeric($v2)? Base::instance()->sign($v1-$v2):strcmp($v1,$v2); if ($order==SORT_DESC) @@ -96,12 +96,15 @@ class Matrix extends Prefab { * @param $first int **/ function calendar($date='now',$first=0) { - $parts=getdate(strtotime($date)); - $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); - $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; - $out=array(); - for ($i=0;$i<$days;$i++) - $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; + $out=FALSE; + if (extension_loaded('calendar')) { + $parts=getdate(strtotime($date)); + $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); + $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; + $out=[]; + for ($i=0;$i<$days;$i++) + $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; + } return $out; } diff --git a/app/lib/session.php b/app/lib/session.php index 8b28c936..583193d9 100644 --- a/app/lib/session.php +++ b/app/lib/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -33,7 +33,9 @@ class Session { //! IP, $_ip, //! Suspect callback - $onsuspect; + $onsuspect, + //! Cache instance + $_cache; /** * Open session @@ -61,11 +63,12 @@ class Session { **/ function read($id) { $this->sid=$id; - if (!$data=Cache::instance()->get($id.'.@')) + if (!$data=$this->_cache->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))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -85,13 +88,13 @@ class Session { function write($id,$data) { $fw=Base::instance(); $jar=$fw->get('JAR'); - Cache::instance()->set($id.'.@', - array( + $this->_cache->set($id.'.@', + [ 'data'=>$data, 'ip'=>$this->_ip, 'agent'=>$this->_agent, 'stamp'=>time() - ), + ], $jar['expire']?($jar['expire']-time()):0 ); return TRUE; @@ -103,7 +106,7 @@ class Session { * @param $id string **/ function destroy($id) { - Cache::instance()->clear($id.'.@'); + $this->_cache->clear($id.'.@'); return TRUE; } @@ -113,7 +116,7 @@ class Session { * @param $max int **/ function cleanup($max) { - Cache::instance()->reset('.@',$max); + $this->_cache->reset('.@',$max); return TRUE; } @@ -148,7 +151,7 @@ class Session { function stamp() { if (!$this->sid) session_start(); - return Cache::instance()->exists($this->sid.'.@',$data)? + return $this->_cache->exists($this->sid.'.@',$data)? $data['stamp']:FALSE; } @@ -165,21 +168,21 @@ class Session { * @param $onsuspect callback * @param $key string **/ - function __construct($onsuspect=NULL,$key=NULL) { + function __construct($onsuspect=NULL,$key=NULL,$cache=null) { $this->onsuspect=$onsuspect; + $this->_cache=$cache?:Cache::instance(); session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/smtp.php b/app/lib/smtp.php index 3c9e964f..00ee9b2d 100644 --- a/app/lib/smtp.php +++ b/app/lib/smtp.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -118,20 +118,41 @@ class SMTP extends Magic { * @return string * @param $cmd string * @param $log bool + * @param $mock bool **/ - protected function dialog($cmd=NULL,$log=TRUE) { - $socket=&$this->socket; - if (!is_null($cmd)) - fputs($socket,$cmd."\r\n"); + protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { $reply=''; - while (!feof($socket) && ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && $str=fgets($socket,4096)) { - $reply.=$str; - if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) + if ($mock) { + $host=str_replace('ssl://','',$this->host); + switch ($cmd) { + case NULL: + $reply='220 '.$host.' ESMTP ready'."\n"; break; + case 'DATA': + $reply='354 Go ahead'."\n"; + break; + case 'QUIT': + $reply='221 '.$host.' closing connection'."\n"; + break; + default: + $reply='250 OK'."\n"; + break; + } + } + else { + $socket=&$this->socket; + if ($cmd) + fputs($socket,$cmd."\r\n"); + while (!feof($socket) && ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && $str=fgets($socket,4096)) { + $reply.=$str; + if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) + break; + } } if ($log) { - $this->log.=$cmd."\n"; + if ($cmd) + $this->log.=$cmd."\n"; $this->log.=str_replace("\r",'',$reply); } return $reply; @@ -147,9 +168,9 @@ class SMTP extends Magic { function attach($file,$alias=NULL,$cid=NULL) { if (!is_file($file)) user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); - if (is_string($alias)) - $file=array($alias=>$file); - $this->attachments[]=array('filename'=>$file,'cid'=>$cid); + if ($alias) + $file=[$alias=>$file]; + $this->attachments[]=['filename'=>$file,'cid'=>$cid]; } /** @@ -157,8 +178,9 @@ class SMTP extends Magic { * @return bool * @param $message string * @param $log bool + * @param $mock bool **/ - function send($message,$log=TRUE) { + function send($message,$log=TRUE,$mock=FALSE) { if ($this->scheme=='ssl' && !extension_loaded('openssl')) return FALSE; // Message should not be blank @@ -168,20 +190,25 @@ class SMTP extends Magic { // Retrieve headers $headers=$this->headers; // Connect to the server - $socket=&$this->socket; - $socket=@fsockopen($this->host,$this->port); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); + if (!$mock) { + $socket=&$this->socket; + $socket=@fsockopen($this->host,$this->port,$errno,$errstr); + if (!$socket) { + $fw->error(500,$errstr); + return FALSE; + } + stream_set_blocking($socket,TRUE); + } // Get server's initial response - $this->dialog(NULL,FALSE); + $this->dialog(NULL,TRUE,$mock); // Announce presence - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); + $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log,$mock); if (strtolower($this->scheme)=='tls') { - $this->dialog('STARTTLS',$log); - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); + $this->dialog('STARTTLS',$log,$mock); + if (!$mock) + stream_socket_enable_crypto( + $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); + $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log,$mock); } if (preg_match('/8BITMIME/',$reply)) $headers['Content-Transfer-Encoding']='8bit'; @@ -192,12 +219,22 @@ class SMTP extends Magic { } if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { // Authenticate - $this->dialog('AUTH LOGIN',$log); - $this->dialog(base64_encode($this->user),$log); - $this->dialog(base64_encode($this->pw),$log); + $this->dialog('AUTH LOGIN',$log,$mock); + $this->dialog(base64_encode($this->user),$log,$mock); + $auth_rply=$this->dialog(base64_encode($this->pw),$log,$mock); + if (!preg_match('/^235\s.*/',$auth_rply)) { + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) + fclose($socket); + return FALSE; + } } + if (empty($headers['Message-ID'])) + $headers['Message-ID']='<'.uniqid('',TRUE).'@'.$this->host.'>'; + if (empty($headers['Date'])) + $headers['Date']=date('r'); // Required headers - $reqd=array('From','To','Subject'); + $reqd=['From','To','Subject']; foreach ($reqd as $id) if (empty($headers[$id])) user_error(sprintf(self::E_Header,$id),E_USER_ERROR); @@ -205,21 +242,32 @@ class SMTP extends Magic { $str=''; // Stringify headers foreach ($headers as $key=>&$val) { - if (!in_array($key,$reqd) && (!$this->attachments || - $key!='Content-Type' && $key!='Content-Transfer-Encoding')) + 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.'>'; + if (in_array($key,['From','To','Cc','Bcc'])) { + $email=''; + preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/', + $val,$matches,PREG_SET_ORDER); + foreach ($matches as $raw) + $email.=($email?', ':''). + (preg_match('/<.+?>/',$raw[0])? + $raw[0]: + ('<'.$raw[0].'>')); + $val=$email; + } unset($val); } // Start message dialog - $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log); + $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log,$mock); foreach ($fw->split($headers['To']. (isset($headers['Cc'])?(';'.$headers['Cc']):''). - (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) - $this->dialog('RCPT TO: '.strstr($dst,'<'),$log); - $this->dialog('DATA',$log); + (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { + $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); + } + $this->dialog('DATA',$log,$mock); if ($this->attachments) { // Replace Content-Type $type=$headers['Content-Type']; @@ -241,28 +289,25 @@ class SMTP extends Magic { $out.=$str.$eol; $out.=$message.$eol; foreach ($this->attachments as $attachment) { - if (is_array($attachment['filename'])) { + if (is_array($attachment['filename'])) list($alias,$file)=each($attachment['filename']); - $filename=$alias; - $attachment['filename']=$file; - } else - $filename=basename($attachment['filename']); + $alias=basename($file=$attachment['filename']); $out.='--'.$hash.$eol; $out.='Content-Type: application/octet-stream'.$eol; $out.='Content-Transfer-Encoding: base64'.$eol; if ($attachment['cid']) $out.='Content-ID: '.$attachment['cid'].$eol; $out.='Content-Disposition: attachment; '. - 'filename="'.$filename.'"'.$eol; + 'filename="'.$alias.'"'.$eol; $out.=$eol; $out.=chunk_split(base64_encode( - file_get_contents($attachment['filename']))).$eol; + file_get_contents($file))).$eol; } $out.=$eol; $out.='--'.$hash.'--'.$eol; $out.='.'; - $this->dialog($out,FALSE); + $this->dialog($out,TRUE,$mock); } else { // Send mail headers @@ -274,10 +319,10 @@ class SMTP extends Magic { $out.=$message.$eol; $out.='.'; // Send message - $this->dialog($out); + $this->dialog($out,TRUE,$mock); } - $this->dialog('QUIT',$log); - if ($socket) + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) fclose($socket); return TRUE; } @@ -290,12 +335,13 @@ class SMTP extends Magic { * @param $user string * @param $pw string **/ - function __construct($host='localhost',$port=25,$scheme=null,$user=null,$pw=null) { - $this->headers=array( + function __construct( + $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL) { + $this->headers=[ 'MIME-Version'=>'1.0', 'Content-Type'=>'text/plain; '. 'charset='.Base::instance()->get('ENCODING') - ); + ]; $this->host=$host; if (strtolower($this->scheme=strtolower($scheme))=='ssl') $this->host='ssl://'.$host; diff --git a/app/lib/template.php b/app/lib/template.php index 8b985cde..c9c0d61b 100644 --- a/app/lib/template.php +++ b/app/lib/template.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,7 +32,7 @@ class Template extends Preview { //! Template tags $tags, //! Custom tag handlers - $custom=array(); + $custom=[]; /** * Template -set- tag handler @@ -60,14 +60,14 @@ class Template extends Preview { ($attrib['with']=$this->token($attrib['with'])) && preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', $attrib['with'],$pairs,PREG_SET_ORDER)? - 'array('.implode(',', + ('['.implode(',', array_map(function($pair) { return '\''.$pair[1].'\'=>'. (preg_match('/^\'.*\'$/',$pair[2]) || preg_match('/\$/',$pair[2])? $pair[2]: \Base::instance()->stringify($pair[2])); - },$pairs)).')+get_defined_vars()': + },$pairs)).']+get_defined_vars()'): 'get_defined_vars()'; $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; return @@ -127,7 +127,7 @@ class Template extends Preview { (isset($attrib['counter'])? (($ctr=$this->token($attrib['counter'])).'=0; '):''). 'foreach (('. - $this->token($attrib['group']).'?:array()) as '. + $this->token($attrib['group']).'?:[]) as '. (isset($attrib['key'])? ($this->token($attrib['key']).'=>'):''). $this->token($attrib['value']).'):'. @@ -147,12 +147,12 @@ class Template extends Preview { // Grab and blocks foreach ($node as $pos=>$block) if (isset($block['true'])) - $true=array($pos,$block); + $true=[$pos,$block]; elseif (isset($block['false'])) - $false=array($pos,$block); + $false=[$pos,$block]; if (isset($true,$false) && $true[0]>$false[0]) // Reverse and blocks - list($node[$true[0]],$node[$false[0]])=array($false[1],$true[1]); + list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; return 'token($attrib['if']).'): ?>'. $this->build($node). @@ -229,7 +229,7 @@ class Template extends Preview { * @return string * @param $node array|string **/ - protected function build($node) { + function build($node) { if (is_string($node)) return parent::build($node); $out=''; @@ -259,7 +259,7 @@ class Template extends Preview { if ($func[0]=='_') return call_user_func_array($this->custom[$func],$args); if (method_exists($this,$func)) - return call_user_func_array(array($this,$func),$args); + return call_user_func_array([$this,$func],$args); user_error(sprintf(self::E_Method,$func),E_USER_ERROR); } @@ -270,9 +270,9 @@ class Template extends Preview { **/ function parse($text) { // Build tree structure - for ($ptr=0,$w=5,$len=strlen($text),$tree=array(),$tmp='';$ptr<$len;) + for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. - '('.$this->tags.')\b((?:\h+[\w-]+'. + '('.$this->tags.')\b((?:\s+[\w-]+'. '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. '\h*\{\{.+?\}\})*)\h*(\/?)>/is', substr($text,$ptr),$match)) { @@ -281,7 +281,7 @@ class Template extends Preview { // Element node if ($match[2]) { // Find matching start tag - $stack=array(); + $stack=[]; for($i=count($tree)-1;$i>=0;$i--) { $item = $tree[$i]; if (is_array($item) && array_key_exists($match[3],$item) @@ -296,7 +296,7 @@ class Template extends Preview { else { // Start tag $node=&$tree[][$match[3]]; - $node=array(); + $node=[]; if ($match[4]) { // Process attributes preg_match_all( diff --git a/app/lib/test.php b/app/lib/test.php index 05e6e6c5..2070dab4 100644 --- a/app/lib/test.php +++ b/app/lib/test.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,7 +32,7 @@ class Test { protected //! Test results - $data=array(), + $data=[], //! Success indicator $passed=TRUE; @@ -61,7 +61,7 @@ class Test { function expect($cond,$text=NULL) { $out=(bool)$cond; if ($this->level==$out || $this->level==self::FLAG_Both) { - $data=array('status'=>$out,'text'=>$text,'source'=>NULL); + $data=['status'=>$out,'text'=>$text,'source'=>NULL]; foreach (debug_backtrace() as $frame) if (isset($frame['file'])) { $data['source']=Base::instance()-> diff --git a/app/lib/utf.php b/app/lib/utf.php index fbfe0005..22b41c3d 100644 --- a/app/lib/utf.php +++ b/app/lib/utf.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -180,7 +180,7 @@ class UTF extends Prefab { * @param $str string **/ function emojify($str) { - $map=array( + $map=[ ':('=>'\u2639', // frown ':)'=>'\u263a', // smile '<3'=>'\u2665', // heart @@ -191,7 +191,7 @@ class UTF extends Prefab { ':,'=>'\u1f60f', // think ':/'=>'\u1f623', // skeptic '8O'=>'\u1f632', // oops - )+Base::instance()->get('EMOJI'); + ]+Base::instance()->get('EMOJI'); return $this->translate(str_replace(array_keys($map), array_values($map),$str)); } diff --git a/app/lib/web.php b/app/lib/web.php index 57fbf937..22864898 100644 --- a/app/lib/web.php +++ b/app/lib/web.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -39,7 +39,7 @@ class Web extends Prefab { **/ function mime($file) { if (preg_match('/\w+$/',$file,$ext)) { - $map=array( + $map=[ 'au'=>'audio/basic', 'avi'=>'video/avi', 'bmp'=>'image/bmp', @@ -77,7 +77,7 @@ class Web extends Prefab { 'xls'=>'application/vnd.ms-excel', 'xml'=>'application/xml', 'zip'=>'application/x-zip-compressed' - ); + ]; foreach ($map as $key=>$val) if (preg_match('/'.$key.'/',strtolower($ext[0]))) return $val; @@ -93,7 +93,7 @@ class Web extends Prefab { * @param $list string|array **/ function acceptable($list=NULL) { - $accept=array(); + $accept=[]; foreach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT'])) as $mime) if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts)) @@ -124,8 +124,10 @@ class Web extends Prefab { * @param $mime string * @param $kbps int * @param $force bool + * @param $name string + * @param $flush bool **/ - function send($file,$mime=NULL,$kbps=0,$force=TRUE) { + function send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) { if (!is_file($file)) return FALSE; $size=filesize($file); @@ -133,7 +135,7 @@ class Web extends Prefab { header('Content-Type: '.($mime?:$this->mime($file))); if ($force) header('Content-Disposition: attachment; '. - 'filename="'.basename($file).'"'); + 'filename="'.($name!==NULL?$name:basename($file)).'"'); header('Accept-Ranges: bytes'); header('Content-Length: '.$size); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); @@ -152,6 +154,10 @@ class Web extends Prefab { } // Send 1KiB and reset timer echo fread($handle,1024); + if ($flush) { + ob_flush(); + flush(); + } } fclose($handle); return $size; @@ -170,9 +176,7 @@ class Web extends Prefab { if (!is_dir($dir)) mkdir($dir,Base::MODE,TRUE); if ($fw->get('VERB')=='PUT') { - $tmp=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(uniqid()); + $tmp=$fw->get('TEMP').$fw->get('SEED').'.'.$fw->hash(uniqid()); if (!$fw->get('RAW')) $fw->write($tmp,$fw->get('BODY')); else { @@ -188,7 +192,7 @@ class Web extends Prefab { fclose($src); } $base=basename($fw->get('URI')); - $file=array( + $file=[ 'name'=>$dir. ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? (is_callable($slug)? @@ -199,24 +203,24 @@ class Web extends Prefab { 'tmp_name'=>$tmp, 'type'=>$this->mime($base), 'size'=>filesize($tmp) - ); + ]; return (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,array($file))!==FALSE) && + (!$func || $fw->call($func,[$file])!==FALSE) && rename($tmp,$file['name']); } - $fetch=function($arr)use(&$fetch){ + $fetch=function($arr) use(&$fetch) { if (!is_array($arr)) - return array($arr); - $data=array(); + return [$arr]; + $data=[]; foreach($arr as $k=>$sub) $data=array_merge($data,$fetch($sub)); return $data; }; - $out=array(); + $out=[]; foreach ($_FILES as $name=>$item) { - $files=array(); - foreach($item as $k=>$mix) - foreach($fetch($mix) as $i=>$val) + $files=[]; + foreach ($item as $k=>$mix) + foreach ($fetch($mix) as $i=>$val) $files[$i][$k]=$val; foreach ($files as $file) { if (empty($file['name'])) @@ -230,9 +234,8 @@ class Web extends Prefab { (isset($parts[2])?$parts[2]:''))): $base); $out[$file['name']]=!$file['error'] && - is_uploaded_file($file['tmp_name']) && (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,array($file,$name))!==FALSE) && + (!$func || $fw->call($func,[$file,$name])!==FALSE) && move_uploaded_file($file['tmp_name'],$file['name']); } } @@ -277,7 +280,7 @@ class Web extends Prefab { ini_get('default_socket_timeout'); curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout); curl_setopt($curl,CURLOPT_TIMEOUT,$timeout); - $headers=array(); + $headers=[]; curl_setopt($curl,CURLOPT_HEADERFUNCTION, // Callback for response headers function($curl,$line) use(&$headers) { @@ -286,22 +289,32 @@ class Web extends Prefab { return strlen($line); } ); + curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2); curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE); ob_start(); curl_exec($curl); + $err=curl_error($curl); curl_close($curl); $body=ob_get_clean(); - if ($options['follow_location'] && + if (!$err && + $options['follow_location'] && preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { $options['max_redirects']--; + if($loc[1][0] == '/') { + $parts=parse_url($url); + $loc[1]=$parts['scheme'].'://'.$parts['host']. + ((isset($parts['port']) && !in_array($parts['port'],[80,443])) + ?':'.$parts['port']:'').$loc[1]; + } return $this->request($loc[1],$options); } - return array( + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'cURL', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -314,28 +327,36 @@ class Web extends Prefab { $eol="\r\n"; $options['header']=implode($eol,$options['header']); $body=@file_get_contents($url,FALSE, - stream_context_create(array('http'=>$options))); + stream_context_create(['http'=>$options])); $headers=isset($http_response_header)? - $http_response_header:array(); - $match=NULL; - foreach ($headers as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); + $http_response_header:[]; + $err=''; + if (is_string($body)) { + $match=NULL; + foreach ($headers as $header) + if (preg_match('/Content-Encoding: (.+)/',$header,$match)) break; - case 'deflate': - $body=gzuncompress($body); - break; - } - return array( + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + } + else { + $tmp=error_get_last(); + $err=$tmp['message']; + } + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'stream', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -346,7 +367,7 @@ class Web extends Prefab { **/ protected function _socket($url,$options) { $eol="\r\n"; - $headers=array(); + $headers=[]; $body=''; $parts=parse_url($url); $empty=empty($parts['port']); @@ -361,54 +382,54 @@ class Web extends Prefab { $parts['path']='/'; if (empty($parts['query'])) $parts['query']=''; - $socket=@fsockopen($parts['host'],$parts['port']); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); - 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 - ); - fputs($socket,implode($eol,$options['header']).$eol.$eol); - if (isset($options['content'])) - fputs($socket,$options['content'].$eol); - // Get response - $content=''; - while (!feof($socket) && - ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && !connection_aborted() && - $str=fgets($socket,4096)) - $content.=$str; - fclose($socket); - $html=explode($eol.$eol,$content,2); - $body=isset($html[1])?$html[1]:''; - $headers=array_merge($headers,$current=explode($eol,$html[0])); - $match=NULL; - foreach ($current as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); - break; - case 'deflate': - $body=gzuncompress($body); + if ($socket=@fsockopen($parts['host'],$parts['port'],$code,$err)) { + stream_set_blocking($socket,TRUE); + 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 + ); + fputs($socket,implode($eol,$options['header']).$eol.$eol); + if (isset($options['content'])) + fputs($socket,$options['content'].$eol); + // Get response + $content=''; + while (!feof($socket) && + ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && !connection_aborted() && + $str=fgets($socket,4096)) + $content.=$str; + fclose($socket); + $html=explode($eol.$eol,$content,2); + $body=isset($html[1])?$html[1]:''; + $headers=array_merge($headers,$current=explode($eol,$html[0])); + $match=NULL; + foreach ($current as $header) + if (preg_match('/Content-Encoding: (.+)/',$header,$match)) break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + if ($options['follow_location'] && + preg_match('/Location: (.+?)'.preg_quote($eol).'/', + $html[0],$loc)) { + $options['max_redirects']--; + return $this->request($loc[1],$options); } - if ($options['follow_location'] && - preg_match('/Location: (.+?)'.preg_quote($eol).'/', - $html[0],$loc)) { - $options['max_redirects']--; - return $this->request($loc[1],$options); } - return array( + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'socket', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -419,17 +440,17 @@ class Web extends Prefab { **/ function engine($arg='curl') { $arg=strtolower($arg); - $flags=array( + $flags=[ 'curl'=>extension_loaded('curl'), 'stream'=>ini_get('allow_url_fopen'), 'socket'=>function_exists('fsockopen') - ); + ]; if ($flags[$arg]) return $this->wrapper=$arg; foreach ($flags as $key=>$val) if ($val) return $this->wrapper=$key; - user_error(E_Request,E_USER_ERROR); + user_error(self::E_Request,E_USER_ERROR); } /** @@ -440,7 +461,7 @@ class Web extends Prefab { **/ function subst(array &$old,$new) { if (is_string($new)) - $new=array($new); + $new=[$new]; foreach ($new as $hdr) { $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', $old,PREG_GREP_INVERT); @@ -469,11 +490,11 @@ class Web extends Prefab { elseif (!preg_match('/https?/',$parts['scheme'])) return FALSE; if (!is_array($options)) - $options=array(); + $options=[]; if (empty($options['header'])) - $options['header']=array(); + $options['header']=[]; elseif (is_string($options['header'])) - $options['header']=array($options['header']); + $options['header']=[$options['header']]; if (!$this->wrapper) $this->engine(); if ($this->wrapper!='stream') { @@ -487,13 +508,13 @@ class Web extends Prefab { $this->subst($options['header'],'Host: '.$parts['host']); } $this->subst($options['header'], - array( + [ 'Accept-Encoding: gzip,deflate', 'User-Agent: '.(isset($options['user_agent'])? $options['user_agent']: 'Mozilla/5.0 (compatible; '.php_uname('s').')'), 'Connection: close' - ) + ] ); if (isset($options['content']) && is_string($options['content'])) { if ($options['method']=='POST' && @@ -508,13 +529,13 @@ class Web extends Prefab { 'Authorization: Basic '. base64_encode($parts['user'].':'.$parts['pass']) ); - $options+=array( + $options+=[ 'method'=>'GET', 'header'=>$options['header'], 'follow_location'=>TRUE, 'max_redirects'=>20, 'ignore_errors'=>FALSE - ); + ]; $eol="\r\n"; if ($fw->get('CACHE') && preg_match('/GET|HEAD/',$options['method'])) { @@ -534,11 +555,14 @@ class Web extends Prefab { $result=$cache->get($hash); $result['cached']=TRUE; } - elseif (preg_match('/Cache-Control: max-age=(.+?)'. - preg_quote($eol).'/',implode($eol,$result['headers']),$exp)) + elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. + preg_quote($eol).')/',implode($eol,$result['headers']),$exp)) $cache->set($hash,$result,$exp[1]); } - return $result; + $req=[$options['method'].' '.$url]; + foreach ($options['header'] as $header) + array_push($req,$header); + return array_merge(['request'=>$req],$result); } /** @@ -563,7 +587,9 @@ class Web extends Prefab { $path=$fw->get('UI').';./'; foreach ($fw->split($path,FALSE) as $dir) foreach ($files as $file) - if (is_file($save=$fw->fixslashes($dir.$file))) { + if (is_file($save=$fw->fixslashes($dir.$file)) && + is_bool(strpos($save,'../')) && + preg_match('/\.(css|js)$/i',$file)) { if ($fw->get('CACHE') && ($cached=$cache->exists( $hash=$fw->hash($save).'.'.$ext[0],$data)) && @@ -637,7 +663,7 @@ class Web extends Prefab { } continue; } - if (in_array($src[$ptr],array('\'','"'))) { + if (in_array($src[$ptr],['\'','"'])) { $match=$src[$ptr]; $data.=$match; $ptr++; @@ -693,14 +719,14 @@ class Web extends Prefab { NULL,LIBXML_NOBLANKS|LIBXML_NOERROR); if (!is_object($xml)) return FALSE; - $out=array(); + $out=[]; if (isset($xml->channel)) { $out['source']=(string)$xml->channel->title; $max=min($max,count($xml->channel->item)); for ($i=0;$i<$max;$i++) { $item=$xml->channel->item[$i]; - $list=array(''=>NULL)+$item->getnamespaces(TRUE); - $fields=array(); + $list=[''=>NULL]+$item->getnamespaces(TRUE); + $fields=[]; foreach ($list as $ns=>$uri) foreach ($item->children($uri) as $key=>$val) $fields[$ns.($ns?':':'').$key]=(string)$val; @@ -748,7 +774,7 @@ class Web extends Prefab { function slug($text) { return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-', trim(strtr(str_replace('\'','',$text), - array( + [ 'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A', 'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A', 'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C', @@ -796,7 +822,7 @@ class Web extends Prefab { 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'' - )+Base::instance()->get('DIACRITICS'))))),'-'); + ]+Base::instance()->get('DIACRITICS'))))),'-'); } /** @@ -850,8 +876,7 @@ if (!function_exists('gzdecode')) { $fw=Base::instance(); if (!is_dir($tmp=$fw->get('TEMP'))) mkdir($tmp,Base::MODE,TRUE); - file_put_contents($file=$tmp.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + file_put_contents($file=$tmp.'/'.$fw->get('SEED').'.'. $fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX); ob_start(); readgzfile($file); diff --git a/app/lib/web/geo.php b/app/lib/web/geo.php index 498f857d..7d7a0bce 100644 --- a/app/lib/web/geo.php +++ b/app/lib/web/geo.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -34,14 +34,14 @@ class Geo extends \Prefab { $ref=new \DateTimeZone($zone); $loc=$ref->getLocation(); $trn=$ref->getTransitions($now=time(),$now); - $out=array( + $out=[ 'offset'=>$ref-> - getOffset(new \DateTime('now',new \DateTimeZone('GMT')))/3600, + getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/3600, 'country'=>$loc['country_code'], 'latitude'=>$loc['latitude'], 'longitude'=>$loc['longitude'], 'dst'=>$trn[0]['isdst'] - ); + ]; unset($ref); return $out; } @@ -72,7 +72,7 @@ class Geo extends \Prefab { if (($req=$web->request('http://www.geoplugin.net/json.gp'. ($public?('?ip='.$ip):''))) && $data=json_decode($req['body'],TRUE)) { - $out=array(); + $out=[]; foreach ($data as $key=>$val) if (!strpos($key,'currency') && $key!=='geoplugin_status' && $key!=='geoplugin_region') @@ -87,17 +87,17 @@ class Geo extends \Prefab { * @return array|FALSE * @param $latitude float * @param $longitude float + * @param $key string **/ - function weather($latitude,$longitude) { + function weather($latitude,$longitude,$key) { $fw=\Base::instance(); $web=\Web::instance(); - $query=array( + $query=[ 'lat'=>$latitude, - 'lon'=>$longitude - ); - $req=$web->request( - 'http://api.openweathermap.org/data/2.5/weather?'. - http_build_query($query)); + 'lon'=>$longitude, + 'APPID'=>$key, + 'units'=>'metric' + ]; return ($req=$web->request( 'http://api.openweathermap.org/data/2.5/weather?'. http_build_query($query)))? diff --git a/app/lib/web/google/staticmap.php b/app/lib/web/google/staticmap.php index 71acc550..6a3f1601 100644 --- a/app/lib/web/google/staticmap.php +++ b/app/lib/web/google/staticmap.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). diff --git a/app/lib/web/oauth2.php b/app/lib/web/oauth2.php new file mode 100644 index 00000000..a41f9fb3 --- /dev/null +++ b/app/lib/web/oauth2.php @@ -0,0 +1,134 @@ +. + +*/ + +namespace Web; + +//! Lightweight OAuth2 client +class OAuth2 extends \Magic { + + protected + //! Scopes and claims + $args=[]; + + /** + * Return OAuth2 authentication URI + * @return string + * @param $endpoint string + **/ + function uri($endpoint) { + return $endpoint.'?'.http_build_query($this->args); + } + + /** + * Send request to API/token endpoint + * @return string|FALSE + * @param $uri string + * @param $method string + * @param $token array + **/ + function request($uri,$method,$token=NULL) { + $web=\Web::instance(); + $options=[ + 'method'=>$method, + 'content'=>http_build_query($this->args), + 'header'=>['Accept: application/json'] + ]; + if ($token) + array_push($options['header'],'Authorization: Bearer '.$token); + elseif ($method=='POST') + array_push($options['header'],'Authorization: Basic '. + base64_encode( + $this->args['client_id'].':'. + $this->args['client_secret'] + ) + ); + $response=$web->request($uri,$options); + return $response['body'] && + preg_grep('/HTTP\/1\.\d 200/',$response['headers'])? + json_decode($response['body'],TRUE): + FALSE; + } + + /** + * Parse JSON Web token + * @return array + * @param $token string + **/ + function jwt($token) { + return json_decode( + base64_decode( + str_replace( + ['-','_'], + ['+','/'], + explode('.',$token)[1] + ) + ), + TRUE + ); + } + + /** + * Return TRUE if scope/claim exists + * @return bool + * @param $key string + **/ + function exists($key) { + return isset($this->args[$key]); + } + + /** + * Bind value to scope/claim + * @return string + * @param $key string + * @param $val string + **/ + function set($key,$val) { + return $this->args[$key]=$val; + } + + /** + * Return value of scope/claim + * @return mixed + * @param $key string + **/ + function &get($key) { + if (isset($this->args[$key])) + $val=&$this->args[$key]; + else + $val=NULL; + return $val; + } + + /** + * Remove scope/claim + * @return NULL + * @param $key + **/ + function clear($key=NULL) { + if ($key) + unset($this->args[$key]); + else + $this->args=[]; + } + +} + diff --git a/app/lib/web/openid.php b/app/lib/web/openid.php index 89173d0b..f795cb37 100644 --- a/app/lib/web/openid.php +++ b/app/lib/web/openid.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -29,7 +29,7 @@ class OpenID extends \Magic { //! OpenID provider endpoint URL $url, //! HTTP request parameters - $args=array(); + $args=[]; /** * Determine OpenID provider @@ -38,11 +38,11 @@ class OpenID extends \Magic { **/ protected function discover($proxy) { // Normalize - if (!preg_match('/https?:\/\//i',$this->args['identity'])) - $this->args['identity']='http://'.$this->args['identity']; - $url=parse_url($this->args['identity']); + if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) + $this->args['endpoint']='http://'.$this->args['endpoint']; + $url=parse_url($this->args['endpoint']); // Remove fragment; reconnect parts - $this->args['identity']=$url['scheme'].'://'. + $this->args['endpoint']=$url['scheme'].'://'. (isset($url['user'])? ($url['user']. (isset($url['pass'])?(':'.$url['pass']):'').'@'):''). @@ -50,7 +50,7 @@ class OpenID extends \Magic { (isset($url['query'])?('?'.$url['query']):''); // HTML-based discovery of OpenID provider $req=\Web::instance()-> - request($this->args['identity'],array('proxy'=>$proxy)); + request($this->args['endpoint'],['proxy'=>$proxy]); if (!$req) return FALSE; $type=array_values(preg_grep('/Content-Type:/',$req['headers'])); @@ -63,8 +63,9 @@ class OpenID extends \Magic { $svc=$xrds['XRD']['Service']; if (isset($svc[0])) $svc=$svc[0]; + $svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'. - '(?:server|signon)/',$svc['Type'])) { + '(?:server|signon)/',$svc_type)) { $this->args['provider']=$svc['URI']; if (isset($svc['LocalID'])) $this->args['localidentity']=$svc['LocalID']; @@ -89,7 +90,7 @@ class OpenID extends \Magic { preg_match_all('/\b(rel|href)\h*=\h*'. '(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr, PREG_SET_ORDER)) { - $node=array(); + $node=[]; foreach ($attr as $kv) $node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3]; if (isset($node['rel']) && @@ -140,7 +141,7 @@ class OpenID extends \Magic { * @param $attr array * @param $reqd string|array **/ - function auth($proxy=NULL,$attr=array(),array $reqd=NULL) { + function auth($proxy=NULL,$attr=[],array $reqd=NULL) { $fw=\Base::instance(); $root=$fw->get('SCHEME').'://'.$fw->get('HOST'); if (empty($this->args['trust_root'])) @@ -157,7 +158,7 @@ class OpenID extends \Magic { $this->args['ax.required']=is_string($reqd)? $reqd:implode(',',$reqd); } - $var=array(); + $var=[]; foreach ($this->args as $key=>$val) $var['openid.'.$key]=$val; $fw->reroute($this->url.'?'.http_build_query($var)); @@ -179,16 +180,16 @@ class OpenID extends \Magic { $this->args['mode']!='error' && $this->url=$this->discover($proxy)) { $this->args['mode']='check_authentication'; - $var=array(); + $var=[]; foreach ($this->args as $key=>$val) $var['openid.'.$key]=$val; $req=\Web::instance()->request( $this->url, - array( + [ 'method'=>'POST', 'content'=>http_build_query($var), 'proxy'=>$proxy - ) + ] ); return (bool)preg_match('/is_valid:true/i',$req['body']); } @@ -245,4 +246,3 @@ class OpenID extends \Magic { } } - diff --git a/app/lib/web/pingback.php b/app/lib/web/pingback.php index 9dd750a3..f063f104 100644 --- a/app/lib/web/pingback.php +++ b/app/lib/web/pingback.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -38,7 +38,7 @@ class Pingback extends \Prefab { $web=\Web::instance(); $req=$web->request($url); $found=FALSE; - if ($req && $req['body']) { + if ($req['body']) { // Look for pingback header foreach ($req['headers'] as $header) if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) { @@ -71,7 +71,7 @@ class Pingback extends \Prefab { $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); $doc->stricterrorchecking=FALSE; $doc->recover=TRUE; - if ($req && @$doc->loadhtml($req['body'])) { + if (@$doc->loadhtml($req['body'])) { // Parse anchor tags $links=$doc->getelementsbytagname('a'); foreach ($links as $link) { @@ -79,17 +79,17 @@ class Pingback extends \Prefab { // Find pingback-enabled resources if ($permalink && $found=$this->enabled($permalink)) { $req=$web->request($found, - array( + [ 'method'=>'POST', 'header'=>'Content-Type: application/xml', 'content'=>xmlrpc_encode_request( 'pingback.ping', - array($source,$permalink), - array('encoding'=>$fw->get('ENCODING')) + [$source,$permalink], + ['encoding'=>$fw->get('ENCODING')] ) - ) + ] ); - if ($req && $req['body']) + if ($req['body']) $this->log.=date('r').' '. $permalink.' [permalink:'.$found.']'.PHP_EOL. $req['body'].PHP_EOL; @@ -118,7 +118,7 @@ class Pingback extends \Prefab { $path=$fw->get('BASE'); $web=\Web::instance(); $args=xmlrpc_decode_request($fw->get('BODY'),$method,$charset); - $options=array('encoding'=>$charset); + $options=['encoding'=>$charset]; if ($method=='pingback.ping' && isset($args[0],$args[1])) { list($source,$permalink)=$args; $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); @@ -138,8 +138,7 @@ class Pingback extends \Prefab { $links=$doc->getelementsbytagname('a'); foreach ($links as $link) { if ($link->getattribute('href')==$permalink) { - call_user_func_array($func, - array($source,$req['body'])); + call_user_func_array($func,[$source,$req['body']]); // Success die(xmlrpc_encode_request(NULL,$source,$options)); } diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 63bd33a5..9e74634c 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -7,7 +7,9 @@ */ namespace Controller; -use Controller\Api as Api; + +use lib\Config; +use lib\Socket; use Model; class AccessController extends Controller { @@ -40,4 +42,32 @@ class AccessController extends Controller { } } + /** + * broadcast map data to clients + * -> send over TCP Socket + * @param Model\MapModel $map + * @return int (number of active connections for this map) + */ + protected function broadcastMapData(Model\MapModel $map){ + $mapData = $this->getFormattedMapData($map); + return (int)(new Socket( Config::getSocketUri() ))->sendData('mapUpdate', $mapData); + } + + /** + * get formatted Map Data + * @param Model\MapModel $map + * @return array + */ + protected function getFormattedMapData(Model\MapModel $map){ + $mapData = $map->getData(); + + return [ + 'config' => $mapData->mapData, + 'data' => [ + 'systems' => $mapData->systems, + 'connections' => $mapData->connections, + ] + ]; + } + } \ No newline at end of file diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php index b217e2bd..2ef66ba5 100644 --- a/app/main/controller/api/connection.php +++ b/app/main/controller/api/connection.php @@ -86,6 +86,9 @@ class Connection extends Controller\AccessController { $connection->save(); $newConnectionData = $connection->getData(); + + // broadcast map changes + $this->broadcastMapData($connection->mapId); } } } @@ -100,18 +103,31 @@ class Connection extends Controller\AccessController { * @throws \Exception */ public function delete(\Base $f3){ - $connectionIds = $f3->get('POST.connectionIds'); - $activeCharacter = $this->getCharacter(); + $mapId = (int)$f3->get('POST.mapId'); + $connectionIds = (array)$f3->get('POST.connectionIds'); - /** - * @var Model\ConnectionModel $connection - */ - $connection = Model\BasicModel::getNew('ConnectionModel'); - foreach($connectionIds as $connectionId){ - $connection->getById($connectionId); - $connection->delete( $activeCharacter ); + if($mapId){ + $activeCharacter = $this->getCharacter(); + + /** + * @var Model\MapModel $map + */ + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapId); + + if( $map->hasAccess($activeCharacter) ){ + foreach($connectionIds as $connectionId){ + if( $connection = $map->getConnectionById($connectionId) ){ + $connection->delete( $activeCharacter ); + + $connection->reset(); + } + } + + // broadcast map changes + $this->broadcastMapData($map); + } - $connection->reset(); } echo json_encode([]); diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 4350ce13..7e178dde 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -9,6 +9,7 @@ namespace Controller\Api; use Controller; use lib\Config; +use lib\Socket; use Model; /** @@ -19,6 +20,7 @@ use Model; class Map extends Controller\AccessController { // cache keys + const CACHE_KEY_INIT = 'CACHED_INIT'; const CACHE_KEY_MAP_DATA = 'CACHED.MAP_DATA.%s'; const CACHE_KEY_USER_DATA = 'CACHED.USER_DATA.%s_%s'; @@ -68,135 +70,131 @@ class Map extends Controller\AccessController { * @param \Base $f3 */ public function init(\Base $f3){ - // expire time in seconds - $expireTimeHead = 60 * 60 * 12; + $expireTimeCache = 60 * 60; $expireTimeSQL = 60 * 60 * 12; - $f3->expire($expireTimeHead); + if( !$f3->exists(self::CACHE_KEY_INIT, $return )){ + $return = (object) []; + $return->error = []; - $return = (object) []; - $return->error = []; + // static program data ---------------------------------------------------------------------------------------- + $return->timer = $f3->get('PATHFINDER.TIMER'); - // static program data ---------------------------------------------------------------------------------------- - $return->timer = $f3->get('PATHFINDER.TIMER'); + // get all available map types -------------------------------------------------------------------------------- + $mapType = Model\BasicModel::getNew('MapTypeModel'); + $rows = $mapType->find('active = 1', null, $expireTimeSQL); - // get all available map types -------------------------------------------------------------------------------- - $mapType = Model\BasicModel::getNew('MapTypeModel'); - $rows = $mapType->find('active = 1', null, $expireTimeSQL); + // default map type config + $mapsDefaultConfig = Config::getMapsDefaultConfig(); - $mapTypeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'class' => $rowData->class, - 'classTab' => $rowData->classTab + $mapTypeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'class' => $rowData->class, + 'classTab' => $rowData->classTab, + 'defaultConfig' => $mapsDefaultConfig[$rowData->name] + ]; + $mapTypeData[$rowData->name] = $data; + + } + $return->mapTypes = $mapTypeData; + + // get all available map scopes ------------------------------------------------------------------------------- + $mapScope = Model\BasicModel::getNew('MapScopeModel'); + $rows = $mapScope->find('active = 1', null, $expireTimeSQL); + $mapScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label + ]; + $mapScopeData[$rowData->name] = $data; + } + $return->mapScopes = $mapScopeData; + + // get all available system status ---------------------------------------------------------------------------- + $systemStatus = Model\BasicModel::getNew('SystemStatusModel'); + $rows = $systemStatus->find('active = 1', null, $expireTimeSQL); + $systemScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'class' => $rowData->class + ]; + $systemScopeData[$rowData->name] = $data; + } + $return->systemStatus = $systemScopeData; + + // get all available system types ----------------------------------------------------------------------------- + $systemType = Model\BasicModel::getNew('SystemTypeModel'); + $rows = $systemType->find('active = 1', null, $expireTimeSQL); + $systemTypeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'name' => $rowData->name + ]; + $systemTypeData[$rowData->name] = $data; + } + $return->systemType = $systemTypeData; + + // get available connection scopes ---------------------------------------------------------------------------- + $connectionScope = Model\BasicModel::getNew('ConnectionScopeModel'); + $rows = $connectionScope->find('active = 1', null, $expireTimeSQL); + $connectionScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'connectorDefinition' => $rowData->connectorDefinition + ]; + $connectionScopeData[$rowData->name] = $data; + } + $return->connectionScopes = $connectionScopeData; + + // get available character status ----------------------------------------------------------------------------- + $characterStatus = Model\BasicModel::getNew('CharacterStatusModel'); + $rows = $characterStatus->find('active = 1', null, $expireTimeSQL); + $characterStatusData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'name' => $rowData->name, + 'class' => $rowData->class + ]; + $characterStatusData[$rowData->name] = $data; + } + $return->characterStatus = $characterStatusData; + + // route search config ---------------------------------------------------------------------------------------- + $return->routeSearch = [ + 'defaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.SEARCH_DEFAULT_COUNT'), + 'maxDefaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.MAX_Default_COUNT'), + 'limit' => $this->getF3()->get('PATHFINDER.ROUTE.LIMIT'), ]; - $mapTypeData[$rowData->name] = $data; - } - $return->mapTypes = $mapTypeData; - - // get all available map scopes ------------------------------------------------------------------------------- - $mapScope = Model\BasicModel::getNew('MapScopeModel'); - $rows = $mapScope->find('active = 1', null, $expireTimeSQL); - $mapScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label + // get program routes ----------------------------------------------------------------------------------------- + $return->routes = [ + 'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] ) ]; - $mapScopeData[$rowData->name] = $data; - } - $return->mapScopes = $mapScopeData; - // get all available system status ---------------------------------------------------------------------------- - $systemStatus = Model\BasicModel::getNew('SystemStatusModel'); - $rows = $systemStatus->find('active = 1', null, $expireTimeSQL); - $systemScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'class' => $rowData->class + // get notification status ------------------------------------------------------------------------------------ + $return->notificationStatus = [ + 'rallySet' => (bool)Config::getNotificationMail('RALLY_SET') ]; - $systemScopeData[$rowData->name] = $data; + + $f3->set(self::CACHE_KEY_INIT, $return, $expireTimeCache ); } - $return->systemStatus = $systemScopeData; - // get all available system types ----------------------------------------------------------------------------- - $systemType = Model\BasicModel::getNew('SystemTypeModel'); - $rows = $systemType->find('active = 1', null, $expireTimeSQL); - $systemTypeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'name' => $rowData->name - ]; - $systemTypeData[$rowData->name] = $data; - } - $return->systemType = $systemTypeData; + // Add data that should not be cached ========================================================================= - // get available connection scopes ---------------------------------------------------------------------------- - $connectionScope = Model\BasicModel::getNew('ConnectionScopeModel'); - $rows = $connectionScope->find('active = 1', null, $expireTimeSQL); - $connectionScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'connectorDefinition' => $rowData->connectorDefinition - ]; - $connectionScopeData[$rowData->name] = $data; - } - $return->connectionScopes = $connectionScopeData; - - // get available character status ----------------------------------------------------------------------------- - $characterStatus = Model\BasicModel::getNew('CharacterStatusModel'); - $rows = $characterStatus->find('active = 1', null, $expireTimeSQL); - $characterStatusData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'name' => $rowData->name, - 'class' => $rowData->class - ]; - $characterStatusData[$rowData->name] = $data; - } - $return->characterStatus = $characterStatusData; - - // get max number of shared entities per map ------------------------------------------------------------------ - $maxSharedCount = [ - 'character' => $f3->get('PATHFINDER.MAP.PRIVATE.MAX_SHARED'), - 'corporation' => $f3->get('PATHFINDER.MAP.CORPORATION.MAX_SHARED'), - 'alliance' => $f3->get('PATHFINDER.MAP.ALLIANCE.MAX_SHARED'), - ]; - $return->maxSharedCount = $maxSharedCount; - - // get activity log options per map --------------------------------------------------------------------------- - $activityLogging = [ - 'character' => $f3->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING'), - 'corporation' => $f3->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING'), - 'alliance' => $f3->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING'), - ]; - $return->activityLogging = $activityLogging; - - // route search config ---------------------------------------------------------------------------------------- - $return->routeSearch = [ - 'defaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.SEARCH_DEFAULT_COUNT'), - 'maxDefaultCount' => $this->getF3()->get('PATHFINDER.ROUTE.MAX_Default_COUNT'), - 'limit' => $this->getF3()->get('PATHFINDER.ROUTE.LIMIT'), - ]; - - // get program routes ----------------------------------------------------------------------------------------- - $return->routes = [ - 'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] ) - ]; - - // get notification status ------------------------------------------------------------------------------------ - $return->notificationStatus = [ - 'rallySet' => (bool)Config::getNotificationMail('RALLY_SET') + // program mode (e.g. "maintenance") -------------------------------------------------------------------------- + $return->programMode = [ + 'maintenance' => $this->getF3()->get('PATHFINDER.LOGIN.MODE_MAINTENANCE') ]; // get SSO error messages that should be shown immediately ---------------------------------------------------- @@ -222,6 +220,7 @@ class Map extends Controller\AccessController { $return = (object) []; $return->error = []; + $return->warning = []; if( isset($importData['typeId']) && @@ -246,96 +245,116 @@ class Map extends Controller\AccessController { $connection = Model\BasicModel::getNew('ConnectionModel'); $connection->setActivityLogging(false); - foreach($importData['mapData'] as $mapData){ - if( - isset($mapData['config']) && - isset($mapData['data']) - ){ + // to many systems for import + $mapTypeModel = Model\BasicModel::getNew('MapTypeModel'); + $mapTypeModel->getById( (int)$importData['typeId'] ); + if( !$mapTypeModel->dry() ){ + $defaultConfig = Config::getMapsDefaultConfig($mapTypeModel->name); + foreach($importData['mapData'] as $mapData){ if( - isset($mapData['data']['systems']) && - isset($mapData['data']['connections']) + isset($mapData['config']) && + isset($mapData['data']) ){ - if(isset($mapData['config']['id'])){ - unset($mapData['config']['id']); - } - $map->setData($mapData['config']); - $map->typeId = (int)$importData['typeId']; - $map->save(); - - // new system IDs will be generated - // therefore we need to temp store a mapping between IDs - $tempSystemIdMapping = []; - - foreach($mapData['data']['systems'] as $systemData){ - if(isset($systemData['id'])){ - $oldId = (int)$systemData['id']; - unset($systemData['id']); - - $system->setData($systemData); - $system->mapId = $map; - $system->createdCharacterId = $activeCharacter; - $system->updatedCharacterId = $activeCharacter; - $system->save(); - - $tempSystemIdMapping[$oldId] = $system->id; - $system->reset(); + if( + isset($mapData['data']['systems']) && + isset($mapData['data']['connections']) + ){ + if(isset($mapData['config']['id'])){ + unset($mapData['config']['id']); } - } - foreach($mapData['data']['connections'] as $connectionData){ - // check if source and target IDs match with new system ID - if( - isset( $tempSystemIdMapping[$connectionData['source']] ) && - isset( $tempSystemIdMapping[$connectionData['target']] ) - ){ - if(isset($connectionData['id'])){ - unset($connectionData['id']); + + $systemCount = count($mapData['data']['systems']); + if( $systemCount <= $defaultConfig['max_systems']){ + + $map->setData($mapData['config']); + $map->typeId = (int)$importData['typeId']; + $map->save(); + + // new system IDs will be generated + // therefore we need to temp store a mapping between IDs + $tempSystemIdMapping = []; + + foreach($mapData['data']['systems'] as $systemData){ + if(isset($systemData['id'])){ + $oldId = (int)$systemData['id']; + unset($systemData['id']); + + $system->setData($systemData); + $system->mapId = $map; + $system->createdCharacterId = $activeCharacter; + $system->updatedCharacterId = $activeCharacter; + $system->save(); + + $tempSystemIdMapping[$oldId] = $system->id; + $system->reset(); + } } - $connection->setData($connectionData); - $connection->mapId = $map; - $connection->source = $tempSystemIdMapping[$connectionData['source']]; - $connection->target = $tempSystemIdMapping[$connectionData['target']]; - $connection->save(); + foreach($mapData['data']['connections'] as $connectionData){ + // check if source and target IDs match with new system ID + if( + isset( $tempSystemIdMapping[$connectionData['source']] ) && + isset( $tempSystemIdMapping[$connectionData['target']] ) + ){ + if(isset($connectionData['id'])){ + unset($connectionData['id']); + } - $connection->reset(); + $connection->setData($connectionData); + $connection->mapId = $map; + $connection->source = $tempSystemIdMapping[$connectionData['source']]; + $connection->target = $tempSystemIdMapping[$connectionData['target']]; + $connection->save(); + + $connection->reset(); + } + } + + // map access info should not automatically imported + if($map->isPrivate()){ + $map->setAccess($activeCharacter); + }elseif($map->isCorporation()){ + if($corporation = $activeCharacter->getCorporation()){ + $map->setAccess($corporation); + } + }elseif($map->isAlliance()){ + if($alliance = $activeCharacter->getAlliance()){ + $map->setAccess($alliance); + } + } + }else{ + $maxSystemsError = (object) []; + $maxSystemsError->type = 'error'; + $maxSystemsError->message = 'Map has to many systems (' . $systemCount . ').' + .' Max system count is ' . $defaultConfig['max_systems'] . ' for ' . $mapTypeModel->name . ' maps.'; + $return->error[] = $maxSystemsError; } + }else{ + // systems || connections missing + $missingConfigError = (object) []; + $missingConfigError->type = 'error'; + $missingConfigError->message = 'Map data not valid (systems || connections) missing'; + $return->error[] = $missingConfigError; } - - // map access info should not automatically imported - if($map->isPrivate()){ - $map->setAccess($activeCharacter); - }elseif($map->isCorporation()){ - if($corporation = $activeCharacter->getCorporation()){ - $map->setAccess($corporation); - } - }elseif($map->isAlliance()){ - if($alliance = $activeCharacter->getAlliance()){ - $map->setAccess($alliance); - } - } - }else{ - // systems || connections missing + // map config || systems/connections missing $missingConfigError = (object) []; $missingConfigError->type = 'error'; - $missingConfigError->message = 'Map data not valid (systems || connections) missing'; + $missingConfigError->message = 'Map data not valid (config || data) missing'; $return->error[] = $missingConfigError; } - }else{ - // map config || systems/connections missing - $missingConfigError = (object) []; - $missingConfigError->type = 'error'; - $missingConfigError->message = 'Map data not valid (config || data) missing'; - $return->error[] = $missingConfigError; + $map->reset(); } - - - $map->reset(); + }else{ + $unknownMapType = (object) []; + $unknownMapType->type = 'error'; + $unknownMapType->message = 'Map type unknown!'; + $return->error[] = $unknownMapType; } }else{ // map data missing @@ -345,7 +364,6 @@ class Map extends Controller\AccessController { $return->error[] = $missingDataError; } - echo json_encode($return); } @@ -388,10 +406,10 @@ class Map extends Controller\AccessController { $maxShared = max($f3->get('PATHFINDER.MAP.PRIVATE.MAX_SHARED') - 1, 0); $accessCharacters = array_slice($accessCharacters, 0, $maxShared); - if($accessCharacters){ - // clear map access. In case something has removed from access list - $map->clearAccess(); + // clear map access. In case something has removed from access list + $map->clearAccess(); + if($accessCharacters){ /** * @var $tempCharacter Model\CharacterModel */ @@ -431,10 +449,10 @@ class Map extends Controller\AccessController { $maxShared = max($f3->get('PATHFINDER.MAP.CORPORATION.MAX_SHARED') - 1, 0); $accessCorporations = array_slice($accessCorporations, 0, $maxShared); - if($accessCorporations){ - // clear map access. In case something has removed from access list - $map->clearAccess(); + // clear map access. In case something has removed from access list + $map->clearAccess(); + if($accessCorporations){ /** * @var $tempCorporation Model\CorporationModel */ @@ -474,10 +492,10 @@ class Map extends Controller\AccessController { $maxShared = max($f3->get('PATHFINDER.MAP.ALLIANCE.MAX_SHARED') - 1, 0); $accessAlliances = array_slice($accessAlliances, 0, $maxShared); - if($accessAlliances){ - // clear map access. In case something has removed from access list - $map->clearAccess(); + // clear map access. In case something has removed from access list + $map->clearAccess(); + if($accessAlliances){ /** * @var $tempAlliance Model\AllianceModel */ @@ -504,7 +522,16 @@ class Map extends Controller\AccessController { } // reload the same map model (refresh) // this makes sure all data is up2date - $map->getById( $map->id, 0 ); + $map->getById( $map->_id, 0 ); + + + $charactersData = $map->getCharactersData(); + $characterIds = array_map(function ($data){ + return $data->id; + }, $charactersData); + + // broadcast map Access -> and send map Data + $this->broadcastMapAccess($map, $characterIds); $return->mapData = $map->getData(); }else{ @@ -538,11 +565,79 @@ class Map extends Controller\AccessController { */ $map = Model\BasicModel::getNew('MapModel'); $map->getById($mapData['id']); - $map->delete( $activeCharacter ); + $map->delete( $activeCharacter, function($mapId){ + $this->broadcastMapDeleted($mapId); + }); echo json_encode([]); } + /** + * broadcast characters with map access rights to WebSocket server + * -> if characters with map access found -> broadcast mapData to them + * @param Model\MapModel $map + * @param array $characterIds + * @return int + */ + protected function broadcastMapAccess($map, $characterIds){ + $connectionCount = 0; + + $mapAccess = [ + 'id' => $map->_id, + 'characterIds' => $characterIds + ]; + $charCount = (int)(new Socket( Config::getSocketUri() ))->sendData('mapAccess', $mapAccess); + + if($charCount > 0){ + // map has active connections that should receive map Data + $connectionCount = $this->broadcastMapData($map); + } + + return $connectionCount; + } + + /** + * broadcast map delete information to clients + * @param int $mapId + * @return bool|string + */ + protected function broadcastMapDeleted($mapId){ + return (new Socket( Config::getSocketUri() ))->sendData('mapDeleted', $mapId); + } + + /** + * get map access tokens for current character + * -> send access tokens via TCP Socket for WebSocket auth + * @param \Base $f3 + */ + public function getAccessData(\Base $f3){ + $return = (object) []; + + $activeCharacter = $this->getCharacter(); + $maps = $activeCharacter->getMaps(); + + $return->data = [ + 'id' => $activeCharacter->_id, + 'token' => bin2hex(random_bytes(16)), // token for character access + 'mapData' => [] + ]; + + if($maps){ + foreach($maps as $map){ + $return->data['mapData'][] = [ + 'id' => $map->_id, + 'token' => bin2hex(random_bytes(16)) // token for map access + ]; + } + } + + // send Access Data to WebSocket Server and get response (status) + // if 'OK' -> Socket exists + $return->status = (new Socket( Config::getSocketUri() ))->sendData('mapConnectionAccess', $return->data); + + echo json_encode( $return ); + } + /** * update map data * -> function is called continuously (trigger) by any active client @@ -554,17 +649,16 @@ class Map extends Controller\AccessController { $activeCharacter = $this->getCharacter(); - $return = (object) []; - $return->error = []; - - $cacheKey = $this->getMapDataCacheKey($activeCharacter); // if there is any system/connection change data submitted -> save new data if( !empty($mapData) || - !$f3->exists($cacheKey) + !$f3->exists($cacheKey, $return) ){ + $return = (object) []; + $return->error = []; + // get current map data =============================================================================== $maps = $activeCharacter->getMaps(); @@ -595,6 +689,7 @@ class Map extends Controller\AccessController { // loop current user maps and check for changes foreach($maps as $map){ + $mapChanged = false; // update system data --------------------------------------------------------------------- foreach($systems as $i => $systemData){ @@ -623,6 +718,8 @@ class Map extends Controller\AccessController { $system->updatedCharacterId = $activeCharacter; $system->save(); + $mapChanged = true; + // a system belongs to ONE map -> speed up for multiple maps unset($systemData[$i]); } @@ -655,26 +752,29 @@ class Map extends Controller\AccessController { $connection->setData($connectionData); $connection->save(); + $mapChanged = true; + // a connection belongs to ONE map -> speed up for multiple maps unset($connectionData[$i]); } } } + + if($mapChanged){ + $this->broadcastMapData($map); + } } } } // format map Data for return - $return->mapData = self::getFormattedMapData($maps); + $return->mapData = $this->getFormattedMapsData($maps); // cache time(s) per user should be equal or less than this function is called // prevent request flooding $responseTTL = (int)$f3->get('PATHFINDER.TIMER.UPDATE_SERVER_MAP.DELAY') / 1000; $f3->set($cacheKey, $return, $responseTTL); - }else{ - // get from cache - $return = $f3->get($cacheKey); } // if userData is requested -> add it as well @@ -688,23 +788,13 @@ class Map extends Controller\AccessController { /** * get formatted map data - * @param $mapModels + * @param Model\MapModel[] $mapModels * @return array */ - public static function getFormattedMapData($mapModels){ + protected function getFormattedMapsData($mapModels){ $mapData = []; - foreach($mapModels as &$mapModel){ - /** - * @var $mapModel Model\MapModel - */ - $allMapData = $mapModel->getData(); - $mapData[] = [ - 'config' => $allMapData->mapData, - 'data' => [ - 'systems' => $allMapData->systems, - 'connections' => $allMapData->connections, - ] - ]; + foreach($mapModels as $mapModel){ + $mapData[] = $this->getFormattedMapData($mapModel); } return $mapData; @@ -717,7 +807,6 @@ class Map extends Controller\AccessController { */ public function updateUserData(\Base $f3){ $return = (object) []; - $return->error = []; $activeCharacter = $this->getCharacter(0); @@ -747,8 +836,12 @@ class Map extends Controller\AccessController { $map = $this->updateMapData($activeCharacter, $map); } + // get from cache + // this should happen if a user has multiple program instances running + // with the same main char $cacheKey = $this->getUserDataCacheKey($mapId, $requestSystemData->systemId); - if( !$f3->exists($cacheKey) ){ + if( !$f3->exists($cacheKey, $return) ){ + $return = (object) []; $return->mapUserData[] = $map->getUserData(); // request signature data for a system if user has map access! @@ -768,11 +861,6 @@ class Map extends Controller\AccessController { // cache response $f3->set($cacheKey, $return, $responseTTL); - }else{ - // get from cache - // this should happen if a user has multiple program instances running - // with the same main char - $return = $f3->get($cacheKey); } } } @@ -781,6 +869,9 @@ class Map extends Controller\AccessController { // even if they have multiple characters using the same map! $return->userData = $activeCharacter->getUser()->getData(); + // add error (if exists) + $return->error = []; + echo json_encode( $return ); } @@ -793,8 +884,8 @@ class Map extends Controller\AccessController { */ protected function updateMapData(Model\CharacterModel $character, Model\MapModel $map){ - // update "map data" cache in case of map (system/connection) changes - $clearMapDataCache = false; + // map changed. update cache (system/connection) changed + $mapDataChanged = false; if( ( $mapScope = $map->getScope() ) && @@ -928,7 +1019,7 @@ class Map extends Controller\AccessController { if($sourceSystem){ $map = $sourceSystem->mapId; $sourceExists = true; - $clearMapDataCache = true; + $mapDataChanged = true; // increase system position (prevent overlapping) $systemPosX = $sourceSystem->posX + $systemOffsetX; $systemPosY = $sourceSystem->posY + $systemOffsetY; @@ -945,7 +1036,7 @@ class Map extends Controller\AccessController { // get updated maps object if($targetSystem){ $map = $targetSystem->mapId; - $clearMapDataCache = true; + $mapDataChanged = true; $targetExists = true; } } @@ -964,14 +1055,15 @@ class Map extends Controller\AccessController { // get updated maps object if($connection){ $map = $connection->mapId; - $clearMapDataCache = true; + $mapDataChanged = true; } } } } - if($clearMapDataCache){ + if($mapDataChanged){ $this->clearMapDataCache($character); + $this->broadcastMapData($map); } return $map; diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index 73ac4463..6d9f5b46 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -53,13 +53,17 @@ class Route extends Controller\AccessController { * -> this function is required for route search! (Don´t forget) * @param array $mapIds * @param array $filterData + * @param array $keepSystems */ - public function initJumpData($mapIds = [], $filterData = []){ + public function initJumpData($mapIds = [], $filterData = [], $keepSystems = []){ // add static data (e.g. K-Space stargates,..) $this->setStaticJumpData(); // add map specific data $this->setDynamicJumpData($mapIds, $filterData); + + // filter jump data (e.g. remove some systems (0.0, LS) + $this->filterJumpData($filterData, $keepSystems); } /** @@ -78,16 +82,10 @@ class Route extends Controller\AccessController { $cacheKeyIdArray = $cacheKey . '.idArray'; if( - $f3->exists($cacheKeyNamedArray) && - $f3->exists($cacheKeyJumpArray) && - $f3->exists($cacheKeyIdArray) + !$f3->exists($cacheKeyNamedArray, $this->nameArray) || + !$f3->exists($cacheKeyJumpArray, $this->jumpArray) || + !$f3->exists($cacheKeyIdArray, $this->idArray) ){ - - // get cached values - $this->nameArray = $f3->get($cacheKeyNamedArray); - $this->jumpArray = $f3->get($cacheKeyJumpArray); - $this->idArray = $f3->get($cacheKeyIdArray); - }else{ // nothing cached $query = "SELECT * FROM system_neighbour"; $rows = $this->getDB()->exec($query, null, $this->staticJumpDataCacheTime); @@ -256,6 +254,35 @@ class Route extends Controller\AccessController { } } + /** + * filter systems (remove some systems) e.g. WH,LS,0.0 for "safer search" + * @param array $filterData + * @param array $keepSystems + */ + private function filterJumpData($filterData = [], $keepSystems = []){ + if($filterData['safer']){ + // remove all systems (TrueSec < 0.5) from search arrays + $this->jumpArray = array_filter($this->jumpArray, function($jumpData) use($keepSystems) { + + // systemId is always last entry + $systemId = end($jumpData); + $systemNameData = $this->nameArray[$systemId]; + $systemSec = $systemNameData[3]; + + if($systemSec < 0.45 && !in_array($systemId, $keepSystems)){ + // remove system from nameArray as well + unset($this->nameArray[$systemId]); + // remove system from idArray as well + $systemName = $systemNameData[0]; + unset($this->idArray[$systemName]); + return false; + }else{ + return true; + } + }); + } + } + /** * get system data by systemId and dataName * @param $systemId @@ -504,7 +531,7 @@ class Route extends Controller\AccessController { array_walk($mapData, function(&$item, &$key, $data){ if( isset($data[1][$key]) ){ - // character has mas access -> do not check again + // character has map access -> do not check again $item = $data[1][$key]; }else{ // check map access for current character @@ -534,7 +561,8 @@ class Route extends Controller\AccessController { 'wormholes' => (bool) $routeData['wormholes'], 'wormholesReduced' => (bool) $routeData['wormholesReduced'], 'wormholesCritical' => (bool) $routeData['wormholesCritical'], - 'wormholesEOL' => (bool) $routeData['wormholesEOL'] + 'wormholesEOL' => (bool) $routeData['wormholesEOL'], + 'safer' => (bool) $routeData['safer'] ]; $returnRoutData = [ @@ -553,8 +581,9 @@ class Route extends Controller\AccessController { count($mapIds) > 0 ){ $systemFrom = $routeData['systemFromData']['name']; + $systemFromId = (int)$routeData['systemFromData']['systemId']; $systemTo = $routeData['systemToData']['name']; - + $systemToId = (int)$routeData['systemToData']['systemId']; $cacheKey = $this->getRouteCacheKey( $mapIds, @@ -563,15 +592,17 @@ class Route extends Controller\AccessController { $filterData ); - if($f3->exists($cacheKey)){ + if($f3->exists($cacheKey, $cachedData)){ // get data from cache - $returnRoutData = $f3->get($cacheKey); + $returnRoutData = $cachedData; }else{ // max search depth for search $searchDepth = $f3->get('PATHFINDER.ROUTE.SEARCH_DEPTH'); // set jump data for following route search - $this->initJumpData($mapIds, $filterData); + // --> don´t filter some systems (e.g. systemFrom, systemTo) even if they are are WH,LS,0.0 + $keepSystems = [$systemFromId, $systemToId]; + $this->initJumpData($mapIds, $filterData, $keepSystems); // no cached route data found $foundRoutData = $this->findRoute($systemFrom, $systemTo, $searchDepth); diff --git a/app/main/controller/api/statistic.php b/app/main/controller/api/statistic.php index b2067732..ab89776e 100644 --- a/app/main/controller/api/statistic.php +++ b/app/main/controller/api/statistic.php @@ -169,7 +169,7 @@ class Statistic extends Controller\AccessController { ]; // date offset condition ---------------------------------------------------------------------------------- - $sqlDateOffset = " AND CONCAT(`log`.`year`, `log`.`week`) BETWEEN :yearWeekStart AND :yearWeekEnd "; + $sqlDateOffset = " AND CONCAT(`log`.`year`, LPAD(`log`.`week`, 2, 0) ) BETWEEN :yearWeekStart AND :yearWeekEnd "; $queryData[':yearWeekStart'] = $this->concatYearWeek($yearStart, $weekStart); $queryData[':yearWeekEnd'] = $this->concatYearWeek($yearEnd, $weekEnd); diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 73546f1e..bf9912d6 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -273,6 +273,9 @@ class System extends Controller\AccessController { $newSystemModel->getById( $systemModel->id, 0); $newSystemModel->clearCacheData(); $newSystemData = $newSystemModel->getData(); + + // broadcast map changes + $this->broadcastMapData($newSystemModel->mapId); } } @@ -348,8 +351,8 @@ class System extends Controller\AccessController { } $cacheKey = 'CACHE_CONSTELLATION_SYSTEMS_' . self::formatHiveKey($constellationId); - if($f3->exists($cacheKey)){ - $return->systemData = $f3->get($cacheKey); + if($f3->exists($cacheKey, $cachedData)){ + $return->systemData = $cachedData; }else{ if($constellationId > 0){ $systemModels = $this->getSystemModelByIds([$constellationId], 'constellationID'); @@ -408,29 +411,41 @@ class System extends Controller\AccessController { * @param \Base $f3 */ public function delete(\Base $f3){ + $mapId = (int)$f3->get('POST.mapId'); $systemIds = (array)$f3->get('POST.systemIds'); - $activeCharacter = $this->getCharacter(); - /** - * @var Model\SystemModel $system - */ - $system = Model\BasicModel::getNew('SystemModel'); - foreach($systemIds as $systemId){ - $system->getById($systemId); - if( $system->hasAccess($activeCharacter) ){ - // check whether system should be deleted OR set "inactive" - if( - empty($system->alias) && - empty($system->description) - ){ - $system->erase(); - }else{ - // keep data -> set "inactive" - $system->setActive(false); - $system->save(); + if($mapId){ + $activeCharacter = $this->getCharacter(); + + /** + * @var Model\MapModel $map + */ + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapId); + + if( $map->hasAccess($activeCharacter) ){ + foreach($systemIds as $systemId){ + if( $system = $map->getSystemById($systemId) ){ + // check whether system should be deleted OR set "inactive" + if( + empty($system->alias) && + empty($system->description) + ){ + $system->erase(); + }else{ + // keep data -> set "inactive" + $system->setActive(false); + $system->save(); + } + + $system->reset(); + } } - $system->reset(); + + // broadcast map changes + $this->broadcastMapData($map); } + } echo json_encode([]); diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index 4a414711..a8cf0efc 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -130,7 +130,7 @@ class Sso extends Api\User{ // redirect to CCP SSO ---------------------------------------------------------------------- // used for "state" check between request and callback - $state = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM)); + $state = bin2hex( openssl_random_pseudo_bytes(12) ); $f3->set(self::SESSION_KEY_SSO_STATE, $state); $urlParams = [ @@ -756,7 +756,7 @@ class Sso extends Api\User{ * @var Model\CorporationModel $corporationModel */ $corporationModel = Model\BasicModel::getNew('CorporationModel'); - $corporationModel->getById($characterData->corporation['id'], 0); + $corporationModel->getById((int)$characterData->corporation['id'], 0); $corporationModel->copyfrom($characterData->corporation); $corporationModel->save(); } @@ -766,7 +766,7 @@ class Sso extends Api\User{ * @var Model\AllianceModel $allianceModel */ $allianceModel = Model\BasicModel::getNew('AllianceModel'); - $allianceModel->getById($characterData->alliance['id'], 0); + $allianceModel->getById((int)$characterData->alliance['id'], 0); $allianceModel->copyfrom($characterData->alliance); $allianceModel->save(); } @@ -776,7 +776,7 @@ class Sso extends Api\User{ * @var Model\CharacterModel $characterModel */ $characterModel = Model\BasicModel::getNew('CharacterModel'); - $characterModel->getById($characterData->character['id'], 0); + $characterModel->getById((int)$characterData->character['id'], 0); $characterModel->copyfrom($characterData->character); $characterModel->corporationId = $corporationModel; $characterModel->allianceId = $allianceModel; diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index c31b0069..e0e0d152 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -10,6 +10,8 @@ namespace Controller; use Controller\Api as Api; use Controller\Ccp\Sso as Sso; use lib\Config; +use lib\Socket; +use Lib\Util; use Model; use DB; @@ -203,12 +205,12 @@ class Controller { // unique "selector" -> to facilitate database look-ups (small size) // -> This is preferable to simply using the database id field, // which leaks the number of active users on the application - $selector = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM)); + $selector = bin2hex( openssl_random_pseudo_bytes(12) ); // generate unique "validator" (strong encryption) // -> plaintext set to user (cookie), hashed version of this in DB - $size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB); - $validator = bin2hex(mcrypt_create_iv($size, MCRYPT_DEV_URANDOM)); + $size = openssl_cipher_iv_length('aes-256-cbc'); + $validator = bin2hex(openssl_random_pseudo_bytes($size) ); // generate unique cookie token $token = hash('sha256', $validator); @@ -447,10 +449,6 @@ class Controller { return $user; } - public function getCharacterSessionData(){ - - } - /** * log out current character * @param \Base $f3 @@ -458,14 +456,16 @@ class Controller { public function logout(\Base $f3){ $params = (array)$f3->get('POST'); - // ---------------------------------------------------------- - // delete server side cookie validation data - // for the active character - if( - $params['clearCookies'] === '1' && - ( $activeCharacter = $this->getCharacter()) - ){ - $activeCharacter->logout(); + if( $activeCharacter = $this->getCharacter() ){ + + if($params['clearCookies'] === '1'){ + // delete server side cookie validation data + // for the active character + $activeCharacter->logout(); + } + + // broadcast logout information to webSocket server + (new Socket( Config::getSocketUri() ))->sendData('characterLogout', $activeCharacter->_id); } // destroy session login data ------------------------------- @@ -477,28 +477,23 @@ class Controller { * @param \Base $f3 */ public function getEveServerStatus(\Base $f3){ - $return = (object) []; - $return->error = []; - // server status can be cached for some seconds $cacheKey = 'eve_server_status'; - if( !$f3->exists($cacheKey) ){ + if( !$f3->exists($cacheKey, $return) ){ + $return = (object) []; + $return->error = []; + $sso = new Sso(); $return->status = $sso->getCrestServerStatus(); if( !$return->status->crestOffline ){ $f3->set($cacheKey, $return, 60); } - }else{ - // get from cache - $return = $f3->get($cacheKey); } echo json_encode($return); } - - /** * get error object is a user is not found/logged of * @return \stdClass @@ -808,6 +803,16 @@ class Controller { return Config::getEnvironmentData($key); } + + /** + * health check for ICP socket -> ping request + * @param $ttl + * @param $load + */ + static function checkTcpSocket($ttl, $load){ + (new Socket( Config::getSocketUri(), $ttl ))->sendData('healthCheck', $load); + } + /** * get required MySQL variable value * @param $key diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php index 64bcbb11..c00a60af 100644 --- a/app/main/controller/logcontroller.php +++ b/app/main/controller/logcontroller.php @@ -138,11 +138,10 @@ class LogController extends \Prefab { public static function getLogger($type){ $f3 = \Base::instance(); $logFiles = $f3->get('PATHFINDER.LOGFILES'); - $logger = null; - if( !empty($logFiles[$type]) ){ - $logFile = $logFiles[$type] . '.log'; - $logger = new \Log($logFile); - } + + $logFileName = empty($logFiles[$type]) ? 'error' : $logFiles[$type]; + $logFile = $logFileName . '.log'; + $logger = new \Log($logFile); return $logger; } diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index b589aac8..414be18f 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -140,6 +140,15 @@ class Setup extends Controller { // js view (file) $f3->set('jsView', 'setup'); + // set render functions (called within template) + $f3->set('cacheType', function(){ + $cacheType = $this->getF3()->get('CACHE'); + if( strpos($cacheType, 'redis') !== false ){ + $cacheType = 'redis'; + } + return $cacheType; + }); + // render view echo \Template::instance()->render( $f3->get('PATHFINDER.VIEW.INDEX') ); } @@ -189,6 +198,9 @@ class Setup extends Controller { // set database connection information $f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns)); + // set socket information + $f3->set('socketInformation', $this->getSocketInformation()); + // set index information $f3->set('indexInformation', $this->getIndexData()); @@ -421,46 +433,40 @@ class Setup extends Controller { 'version' => strstr(PCRE_VERSION, ' ', true), 'check' => version_compare( strstr(PCRE_VERSION, ' ', true), $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), '>=') ], - 'pdo' => [ + 'ext_pdo' => [ 'label' => 'PDO', 'required' => 'installed', - 'version' => extension_loaded('pdo') ? 'installed' : 'not installed', + 'version' => extension_loaded('pdo') ? 'installed' : 'missing', 'check' => extension_loaded('pdo') ], - 'pdoMysql' => [ + 'ext_pdoMysql' => [ 'label' => 'PDO_MYSQL', 'required' => 'installed', - 'version' => extension_loaded('pdo_mysql') ? 'installed' : 'not installed', + 'version' => extension_loaded('pdo_mysql') ? 'installed' : 'missing', 'check' => extension_loaded('pdo_mysql') ], - 'openssl' => [ + 'ext_openssl' => [ 'label' => 'OpenSSL', 'required' => 'installed', - 'version' => extension_loaded('openssl') ? 'installed' : 'not installed', + 'version' => extension_loaded('openssl') ? 'installed' : 'missing', 'check' => extension_loaded('openssl') ], - 'mcrypt' => [ - 'label' => 'Mcrypt', - 'required' => 'installed', - 'version' => (extension_loaded('mcrypt')) ? 'installed' : 'not installed', - 'check' => (extension_loaded('mcrypt')) - ], - 'xml' => [ + 'ext_xml' => [ 'label' => 'XML', 'required' => 'installed', - 'version' => (extension_loaded('xml')) ? 'installed' : 'not installed', - 'check' => (extension_loaded('xml')) + 'version' => extension_loaded('xml') ? 'installed' : 'missing', + 'check' => extension_loaded('xml') ], - 'gd' => [ + 'ext_gd' => [ 'label' => 'GD Library (for Image plugin)', 'required' => 'installed', - 'version' => (extension_loaded('gd') && function_exists('gd_info')) ? 'installed' : 'not installed', + 'version' => (extension_loaded('gd') && function_exists('gd_info')) ? 'installed' : 'missing', 'check' => (extension_loaded('gd') && function_exists('gd_info')) ], - 'curl' => [ + 'ext_curl' => [ 'label' => 'cURL (for Web plugin)', 'required' => 'installed', - 'version' => (extension_loaded('curl') && function_exists('curl_version')) ? 'installed' : 'not installed', + 'version' => (extension_loaded('curl') && function_exists('curl_version')) ? 'installed' : 'missing', 'check' => (extension_loaded('curl') && function_exists('curl_version')) ], 'maxInputVars' => [ @@ -476,6 +482,33 @@ class Setup extends Controller { 'version' => ini_get('max_execution_time'), 'check' => ini_get('max_execution_time') >= $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'), 'tooltip' => 'PHP default = 30. Max execution time for PHP scripts.' + ], + [ + 'label' => 'Redis Server [optional]' + ], + 'ext_redis' => [ + 'label' => 'Redis', + 'required' => $f3->get('REQUIREMENTS.PHP.REDIS'), + 'version' => extension_loaded('redis') ? phpversion('redis') : 'missing', + 'check' => version_compare( phpversion('redis'), $f3->get('REQUIREMENTS.PHP.REDIS'), '>='), + 'tooltip' => 'Redis can replace the default file-caching mechanic. It is much faster!' + ], + [ + 'label' => 'ØMQ TCP sockets [optional]' + ], + 'ext_zmq' => [ + 'label' => 'ZeroMQ extension', + 'required' => $f3->get('REQUIREMENTS.PHP.ZMQ'), + 'version' => extension_loaded('zmq') ? phpversion('zmq') : 'missing', + 'check' => version_compare( phpversion('zmq'), $f3->get('REQUIREMENTS.PHP.ZMQ'), '>='), + 'tooltip' => 'ØMQ PHP extension. Required for WebSocket configuration.' + ], + 'lib_zmq' => [ + 'label' => 'ZeroMQ installation', + 'required' => $f3->get('REQUIREMENTS.LIBS.ZMQ'), + 'version' => (class_exists('ZMQ') && defined('ZMQ::LIBZMQ_VER')) ? \ZMQ::LIBZMQ_VER : 'unknown', + 'check' => version_compare( (class_exists('ZMQ') && defined('ZMQ::LIBZMQ_VER')) ? \ZMQ::LIBZMQ_VER : 0, $f3->get('REQUIREMENTS.LIBS.ZMQ'), '>='), + 'tooltip' => 'ØMQ version. Required for WebSocket configuration.' ] ]; @@ -905,6 +938,60 @@ class Setup extends Controller { return $checkTables; } + /** + * get Socket information (TCP (internal)), (WebSocket (clients)) + * @return array + */ + protected function getSocketInformation(){ + // $ttl for health check + $ttl = 600; + + $heachCheckToken = microtime(true); + + // ping TCP Socket with checkToken + self::checkTcpSocket($ttl, $heachCheckToken); + + $socketInformation = [ + 'tcpSocket' => [ + 'label' => 'Socket (intern) [TCP]', + 'online' => true, + 'data' => [ + [ + 'label' => 'HOST', + 'value' => Config::getEnvironmentData('SOCKET_HOST'), + 'check' => !empty( Config::getEnvironmentData('SOCKET_HOST') ) + ],[ + 'label' => 'PORT', + 'value' => Config::getEnvironmentData('SOCKET_PORT'), + 'check' => !empty( Config::getEnvironmentData('SOCKET_PORT') ) + ],[ + 'label' => 'URI', + 'value' => Config::getSocketUri(), + 'check' => !empty( Config::getSocketUri() ) + ],[ + 'label' => 'timeout (ms)', + 'value' => $ttl, + 'check' => !empty( $ttl ) + ] + ], + 'token' => $heachCheckToken + ], + 'webSocket' => [ + 'label' => 'WebSocket (clients) [HTTP]', + 'online' => false, + 'data' => [ + [ + 'label' => 'URI', + 'value' => '', + 'check' => false + ] + ] + ] + ]; + + return $socketInformation; + } + /** get indexed (cache) data information * @return array */ diff --git a/app/main/db/database.php b/app/main/db/database.php index 83d06f91..baf3433b 100644 --- a/app/main/db/database.php +++ b/app/main/db/database.php @@ -24,14 +24,13 @@ class Database extends \Prefab { * @return SQL */ public function setDB($database = 'PF'){ - $f3 = \Base::instance(); // "Hive" Key for DB storage $dbHiveKey = $this->getDbHiveKey($database); // check if DB connection already exists - if( !$f3->exists( $dbHiveKey ) ){ + if( !$f3->exists($dbHiveKey, $db) ){ if($database === 'CCP'){ // CCP DB $dns = Controller\Controller::getEnvironmentData('DB_CCP_DNS'); @@ -59,11 +58,9 @@ class Database extends \Prefab { // store DB object $f3->set($dbHiveKey, $db); } - - return $db; - }else{ - return $f3->get( $dbHiveKey ); } + + return $db; } /** @@ -72,15 +69,13 @@ class Database extends \Prefab { * @return SQL */ public function getDB($database = 'PF'){ - $f3 = \Base::instance(); $dbHiveKey = $this->getDbHiveKey($database); - - if( $f3->exists( $dbHiveKey ) ){ - return $f3->get( $dbHiveKey ); - }else{ - return $this->setDB($database); + if( !$f3->exists($dbHiveKey, $db) ){ + $db = $this->setDB($database); } + + return $db; } /** diff --git a/app/main/lib/config.php b/app/main/lib/config.php index 3e6e8781..32aafa2b 100644 --- a/app/main/lib/config.php +++ b/app/main/lib/config.php @@ -51,16 +51,20 @@ class Config extends \Prefab { } /** - * set some global framework variables + * set/overwrite some global framework variables original set in config.ini + * -> can be overwritten in environments.ini OR ENV-Vars + * -> see: https://github.com/exodus4d/pathfinder/issues/175 * that depend on environment settings */ protected function setHiveVariables(){ $f3 = \Base::instance(); + // hive keys that can be overwritten + $hiveKeys = ['BASE', 'URL', 'DEBUG', 'CACHE']; - // hive keys that should be overwritten by environment config - $hiveKeys = ['BASE', 'URL', 'DEBUG']; foreach($hiveKeys as $key){ - $f3->set($key, self::getEnvironmentData($key)); + if( !is_null( $var = self::getEnvironmentData($key)) ){ + $f3->set($key,$var); + } } } @@ -138,12 +142,8 @@ class Config extends \Prefab { * @return string|null */ static function getEnvironmentData($key){ - $f3 = \Base::instance(); $hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key; - $data = null; - if( $f3->exists($hiveKey) ){ - $data = $f3->get($hiveKey); - } + \Base::instance()->exists($hiveKey, $data); return $data; } @@ -157,11 +157,44 @@ class Config extends \Prefab { $f3 = \Base::instance(); $hiveKey = self::HIVE_KEY_PATHFINDER . '.NOTIFICATION.' . $key; $mail = false; - if( $f3->exists($hiveKey) ){ - $mail = $f3->get($hiveKey); + if( $f3->exists($hiveKey, $cachedMail) ){ + $mail = $cachedMail; } return $mail; } + /** + * get map default config values for map types (private/corp/ally) + * -> read from pathfinder.ini + * @param string $mapType + * @return array + */ + static function getMapsDefaultConfig($mapType = ''){ + $f3 = \Base::instance(); + $hiveKey = 'PATHFINDER.MAP'; + if( !empty($mapType) ){ + $hiveKey .= '.' . strtoupper($mapType); + } + return Util::arrayChangeKeyCaseRecursive( $f3->get($hiveKey) ); + } + + /** + * get URI for TCP socket + * @return bool|string + */ + static function getSocketUri(){ + $uri = false; + + if( + ( $ip = self::getEnvironmentData('SOCKET_HOST') ) && + ( $port = self::getEnvironmentData('SOCKET_PORT') ) + ){ + $uri = 'tcp://' . $ip . ':' . $port; + } + + return $uri; + } + + } \ No newline at end of file diff --git a/app/main/lib/socket.php b/app/main/lib/socket.php new file mode 100644 index 00000000..e3e63b27 --- /dev/null +++ b/app/main/lib/socket.php @@ -0,0 +1,250 @@ + The total timeout for a request is ($tll * $maxRetries) + * @var int + */ + protected $ttl = (self::DEFAULT_TTL_MAX / self::DEFAULT_RETRY_MAX); + + /** + * max retry count for message send + * @var int + */ + protected $maxRetries = self::DEFAULT_RETRY_MAX; + + public function __construct($uri, $ttl = self::DEFAULT_TTL_MAX, $maxRetries = self::DEFAULT_RETRY_MAX){ + $this->setTtl($ttl, $maxRetries); + $this->setSocketUri($uri); + } + + /** + * @param mixed $socketUri + */ + public function setSocketUri($socketUri){ + $this->socketUri = $socketUri; + } + + /** + * @param int $ttl + * @param int $maxRetries + */ + public function setTtl(int $ttl, int $maxRetries){ + if( + $ttl > 0 && + $maxRetries > 0 + ){ + $this->maxRetries = $maxRetries; + $this->ttl = round($ttl / $maxRetries); + } + } + + /** + * init new socket + */ + /* + public function initSocket(){ + if(self::checkRequirements()){ + $context = new \ZMQContext(); + $this->socket = $context->getSocket(\ZMQ::SOCKET_REQ); + // The linger value of the socket. Specifies how long the socket blocks trying flush messages after it has been closed + $this->socket->setSockOpt(\ZMQ::SOCKOPT_LINGER, 0); + } + } */ + + /** + * init new socket + */ + public function initSocket(){ + if(self::checkRequirements()){ + $context = new \ZMQContext(); + $this->socket = $context->getSocket(\ZMQ::SOCKET_PUSH); + } + } + + public function sendData($task, $load = ''){ + $response = false; + + $this->initSocket(); + + if( !$this->socket ){ + // Socket not active (e.g. URI missing) + return $response; + } + + // add task, and wrap data + $send = [ + 'task' => $task, + 'load' => $load + ]; + + $this->socket->connect($this->socketUri); + $this->socket->send(json_encode($send)); + $this->socket->disconnect($this->socketUri); + + $response = 'OK'; + + return $response; + } + + /** + * send data to socket and listen for response + * -> "Request" => "Response" setup + * @param $task + * @param $load + * @return bool|string + */ + /* + public function sendData($task, $load = ''){ + $response = false; + + $this->initSocket(); + + if( !$this->socket ){ + // Socket not active (e.g. URI missing) + return $response; + } + + // add task, and wrap data + $send = [ + 'task' => $task, + 'load' => $load + ]; + + $retriesLeft = $this->maxRetries; + // try sending data + while($retriesLeft){ + // Get list of connected endpoints + $endpoints = $this->socket->getEndpoints(); + if (in_array($this->socketUri, $endpoints['connect'])) { + // disconnect e.g. there was no proper response yet + + $this->socket->disconnect($this->socketUri); + // try new socket connection + $this->initSocket(); + } + + $this->socket->connect($this->socketUri); + $this->socket->send(json_encode($send)); + + $readable = []; + $writable = []; + + $poller = new \ZMQPoll(); + $poller->add($this->socket, \ZMQ::POLL_IN); + + $startTime = microtime(true); + // infinite loop until we get a proper answer + while(true){ + // Amount of events retrieved + $events = 0; + + try{ + // Poll until there is something to do + $events = $poller->poll($readable, $writable, $this->ttl); + $errors = $poller->getLastErrors(); + + if(count($errors) > 0){ + // log errors + foreach($errors as $error){ + LogController::getLogger('SOCKET_ERROR')->write(sprintf(self::ERROR_POLLING, $error)); + } + // break infinite loop + break; + } + }catch(\ZMQPollException $e){ + LogController::getLogger('SOCKET_ERROR')->write(sprintf(self::ERROR_POLLING_FAILED, $e->getMessage() )); + } + + + if($events > 0){ + try{ + $response = $this->socket->recv(); + // everything OK -> stop infinite loop AND retry loop! + break 2; + }catch(\ZMQException $e){ + LogController::getLogger('SOCKET_ERROR')->write(sprintf(self::ERROR_RECV_FAILED, $e->getMessage() )); + } + } + + if((microtime(true) - $startTime) > (self::DEFAULT_RESPONSE_MAX / 1000)){ + // max time for response exceeded + LogController::getLogger('SOCKET_ERROR')->write(sprintf(self::ERROR_SEND_FAILED, self::DEFAULT_RESPONSE_MAX)); + break; + } + + // start inf loop again, no proper answer :( + } + + if(--$retriesLeft <= 0){ + // retry limit exceeded + LogController::getLogger('SOCKET_ERROR')->write(sprintf(self::ERROR_OFFLINE, $this->socketUri, $this->maxRetries, $this->ttl)); + break; + } + } + + $this->socket->disconnect($this->socketUri); + + return $response; + }*/ + + /** + * check whether this installation fulfills all requirements + * -> check for ZMQ PHP extension and installed ZQM version + * -> this does NOT check versions! -> those can be verified on /setup page + * @return bool + */ + static function checkRequirements(){ + $check = false; + + if( + extension_loaded('zmq') && + class_exists('ZMQ') + ){ + $check = true; + } + + return $check; + } + + +} \ No newline at end of file diff --git a/app/main/lib/util.php b/app/main/lib/util.php new file mode 100644 index 00000000..31effd5a --- /dev/null +++ b/app/main/lib/util.php @@ -0,0 +1,27 @@ + recursive + * @param $arr + * @param int $case + * @return array + */ + static function arrayChangeKeyCaseRecursive($arr, $case = CASE_LOWER){ + return array_map( function($item){ + if( is_array($item) ) + $item = self::arrayChangeKeyCaseRecursive($item); + return $item; + },array_change_key_case($arr, $case)); + } +} \ No newline at end of file diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index ca541765..be7277c7 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -27,7 +27,7 @@ abstract class BasicModel extends \DB\Cortex { * -> leave this at a higher value * @var int */ - //protected $ttl = 86400; + protected $ttl = 120; /** * caching for relational data @@ -367,12 +367,8 @@ abstract class BasicModel extends \DB\Cortex { // table cache exists // -> check cache for this row data $cacheKey = $this->getCacheKey($dataCacheKeyPrefix); - if( !is_null($cacheKey) ){ - $f3 = self::getF3(); - if( $f3->exists($cacheKey) ){ - $cacheData = $f3->get( $cacheKey ); - } + self::getF3()->exists($cacheKey, $cacheData); } return $cacheData; diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index e5f2bb6b..2a1c478a 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -384,7 +384,7 @@ class CharacterModel extends BasicModel { if( !empty($whitelistCorporations) && $this->hasCorporation() && - in_array($this->get('corporationId', true), $whitelistCorporations) + in_array((int)$this->get('corporationId', true), $whitelistCorporations) ){ $isAuthorized = true; } @@ -394,7 +394,7 @@ class CharacterModel extends BasicModel { !$isAuthorized && !empty($whitelistAlliance) && $this->hasAlliance() && - in_array($this->get('allianceId', true), $whitelistAlliance) + in_array((int)$this->get('allianceId', true), $whitelistAlliance) ){ $isAuthorized = true; } diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index 159a9413..53796b12 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -15,6 +15,11 @@ class MapModel extends BasicModel { protected $table = 'map'; + /** + * cache key prefix for getCharactersData(); + */ + const DATA_CACHE_KEY_CHARACTER = 'CHARACTERS'; + protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -157,7 +162,7 @@ class MapModel extends BasicModel { // map access $mapData->access = (object) []; - $mapData->access->user = []; + $mapData->access->character = []; $mapData->access->corporation = []; $mapData->access->alliance = []; @@ -233,6 +238,16 @@ class MapModel extends BasicModel { $self->clearCacheData(); } + /** + * see parent + */ + public function clearCacheData(){ + parent::clearCacheData(); + + // clear character data with map access as well! + parent::clearCacheDataWithPrefix(self::DATA_CACHE_KEY_CHARACTER); + } + /** * get blank system model pre-filled with default SDE data * -> check for "inactive" systems on this map first! @@ -356,6 +371,24 @@ class MapModel extends BasicModel { return $systemData; } + /** + * search for a connection by id + * @param int $id + * @return null|ConnectionModel + */ + public function getConnectionById($id){ + /** + * @var $connection ConnectionModel + */ + $connection = $this->rel('connections'); + $result = $connection->findone([ + 'active = 1 AND mapId = :mapId AND id = :id', + ':mapId' => $this->id, + ':id' => $id + ]); + return is_object($result) ? $result : null; + } + /** * get all connections in this map * @return ConnectionModel[] @@ -535,7 +568,7 @@ class MapModel extends BasicModel { * get all character models that are currently online "viewing" this map * @return CharacterModel[] */ - private function getActiveCharacters(){ + private function getAllCharacters(){ $characters = []; if($this->isPrivate()){ @@ -563,17 +596,17 @@ class MapModel extends BasicModel { } /** - * get data for all characters that are currently online "viewing" this map + * get data for ALL characters with map access * -> The result of this function is cached! * @return \stdClass[] */ - private function getCharactersData(){ + public function getCharactersData(){ // check if there is cached data - $charactersData = $this->getCacheData('CHARACTERS'); + $charactersData = $this->getCacheData(self::DATA_CACHE_KEY_CHARACTER); if(is_null($charactersData)){ $charactersData = []; - $characters = $this->getActiveCharacters(); + $characters = $this->getAllCharacters(); foreach($characters as $character){ $charactersData[] = $character->getData(true); @@ -581,7 +614,7 @@ class MapModel extends BasicModel { // cache active characters (if found) if(!empty($charactersData)){ - $this->updateCacheData($charactersData, 'CHARACTERS', 5); + $this->updateCacheData($charactersData, self::DATA_CACHE_KEY_CHARACTER, 5); } } @@ -631,13 +664,20 @@ class MapModel extends BasicModel { /** * delete this map and all dependencies * @param CharacterModel $characterModel + * @param null $callback */ - public function delete(CharacterModel $characterModel){ + public function delete(CharacterModel $characterModel, $callback = null){ + if( !$this->dry() ){ // check if character has access if($this->hasAccess($characterModel)){ // all map related tables will be deleted on cascade - $this->erase(); + if( + $this->erase() && + is_callable($callback) + ){ + $callback($this->_id); + } } } } diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index c6213cb6..f46c9548 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -448,7 +448,7 @@ class SystemModel extends BasicModel { * @return bool */ public function hasAccess(CharacterModel $characterModel){ - return $this->mapId->hasAccess($characterModel); + return ($this->mapId) ? $this->mapId->hasAccess($characterModel) : false; } /** diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 00799a46..412baa8a 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -3,7 +3,7 @@ [PATHFINDER] NAME = Pathfinder ; installed version (used for CSS/JS cache busting) -VERSION = v1.1.7 +VERSION = v1.2.0 ; contact information [optional] CONTACT = https://github.com/exodus4d ; public contact email [optional] @@ -26,6 +26,9 @@ MSG_DISABLED = User registration is currently not allowed ; expire time (in days) for login cookies COOKIE_EXPIRE = 30 +; shows "scheduled maintenance" warning to users (default: 0) +MODE_MAINTENANCE = 0 + ; restrict login to specific corporations/alliances by id (e.g. 1000166,1000080) CORPORATION = ALLIANCE = @@ -51,6 +54,8 @@ LOGIN = templates/view/login.html ; - Users can create/view up to "X" maps of a type ; MAX_SHARED: ; - Max number of shared entities per map +; MAX_SYSTEMS: +; - Max number of active systems per map ; ACTIVITY_LOGGING (0: disable, 1: enable): ; - Whether user activity should be logged for a map type ; - E.g. create/update/delete of systems/connections/signatures @@ -58,18 +63,21 @@ LOGIN = templates/view/login.html LIFETIME = 14 MAX_COUNT = 3 MAX_SHARED = 10 +MAX_SYSTEMS = 50 ACTIVITY_LOGGING = 1 [PATHFINDER.MAP.CORPORATION] LIFETIME = 99999 MAX_COUNT = 3 MAX_SHARED = 3 +MAX_SYSTEMS = 100 ACTIVITY_LOGGING = 1 [PATHFINDER.MAP.ALLIANCE] LIFETIME = 99999 MAX_COUNT = 3 MAX_SHARED = 2 +MAX_SYSTEMS = 100 ACTIVITY_LOGGING = 0 ; Route search ==================================================================================== @@ -111,7 +119,7 @@ EXECUTION_LIMIT = 50 ; map user update ping (ajax) (ms) [PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA] DELAY = 5000 -EXECUTION_LIMIT = 300 +EXECUTION_LIMIT = 500 ; update client user data (ms) [PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA] @@ -144,6 +152,8 @@ SESSION_SUSPECT = session_suspect DELETE_ACCOUNT = account_delete ; unauthorized request (HTTP 401) UNAUTHORIZED = unauthorized +; TCP socket errors +SOCKET_ERROR = socket_error ; debug log for development DEBUG = debug diff --git a/app/requirements.ini b/app/requirements.ini index 15c409e0..ef298c1e 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -17,6 +17,12 @@ VERSION = 5.6 ; but needs to be additionally updated on CentOS or Red Hat systems PCRE_VERSION = 8.02 +; Redis extension (optional), required if you want to use Redis as caching Engine (recommended) +REDIS = 3.0.0 + +; ZeroMQ (ØMQ) extension (optional) required for WebSocket Server extension (recommended) +ZMQ = 1.1.3 + ; max execution time for requests MAX_EXECUTION_TIME = 10 @@ -26,6 +32,10 @@ MAX_EXECUTION_TIME = 10 ; PHP default = 1000 MAX_INPUT_VARS = 3000 +[REQUIREMENTS.LIBS] + +ZMQ = 4.1.3 + [REQUIREMENTS.MYSQL] ; min MySQL Version ; newer "deviation" of MySQL like "MariaDB" > 10.1 are recommended @@ -44,6 +54,5 @@ CHARACTER_SET_CONNECTION = utf8 COLLATION_DATABASE = utf8_general_ci COLLATION_CONNECTION = utf8_general_ci FOREIGN_KEY_CHECKS = ON -LOWER_CASE_TABLE_NAMES = 2 diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..7d5b6a20 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "exodus4d/pathfinder", + "description": "Mapping tool for EVE ONLINE", + "minimum-stability": "stable", + "license": "MIT", + "authors": [ + { + "name": "Mark Friedrich", + "email": "pathfinder@exodus4d.de" + } + ], + "autoload": { + "psr-4": { + "Exodus4D\\Pathfinder\\": "app/main" + } + }, + "require": { + "php-64bit": ">=7.0", + "ext-zmq": "1.1.*", + "react/zmq": "0.3.*" + } +} \ No newline at end of file diff --git a/js/app.js b/js/app.js index 7311009d..063467b8 100644 --- a/js/app.js +++ b/js/app.js @@ -26,8 +26,8 @@ requirejs.config({ text: 'lib/requirejs/text', // v2.0.12 A RequireJS/AMD loader plugin for loading text resources. mustache: 'lib/mustache.min', // v1.0.0 Javascript template engine - http://mustache.github.io localForage: 'lib/localforage.min', // v1.4.2 localStorage library - https://mozilla.github.io/localForage - velocity: 'lib/velocity.min', // v1.2.2 animation engine - http://julian.com/research/velocity - velocityUI: 'lib/velocity.ui.min', // v5.0.4 plugin for velocity - http://julian.com/research/velocity/#uiPack + velocity: 'lib/velocity.min', // v1.4.1 animation engine - http://julian.com/research/velocity + velocityUI: 'lib/velocity.ui.min', // v5.2.0 plugin for velocity - http://julian.com/research/velocity/#uiPack slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars jsPlumb: 'lib/dom.jsPlumb-1.7.6', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com farahey: 'lib/farahey-0.5', // v0.5 jsPlumb "magnetizing" extension - https://github.com/jsplumb/farahey diff --git a/js/app/counter.js b/js/app/counter.js index 6bfc90bc..040a0e02 100644 --- a/js/app/counter.js +++ b/js/app/counter.js @@ -5,7 +5,7 @@ define([ ], function($, Init, Util) { 'use strict'; - var config = { + let config = { counterDigitSmallClass: 'pf-digit-counter-small', counterDigitLargeClass: 'pf-digit-counter-large' }; @@ -15,13 +15,13 @@ define([ * @param element * @param tempDate */ - var updateDateDiff = function(element, tempDate){ - var diff = Util.getTimeDiffParts(tempDate, new Date()); - var days = diff.days; - var hrs = diff.hours; - var min = diff.min; - var leftSec = diff.sec; - var value = []; + let updateDateDiff = function(element, tempDate){ + let diff = Util.getTimeDiffParts(tempDate, new Date()); + let days = diff.days; + let hrs = diff.hours; + let min = diff.min; + let leftSec = diff.sec; + let value = []; if( days > 0 || @@ -58,19 +58,22 @@ define([ */ $.fn.initTimestampCounter = function(){ return this.each(function(){ - var element = $(this); - var timestamp = parseInt( element.text() ); + let element = $(this); + let timestamp = parseInt( element.text() ); // do not init twice if(timestamp > 0){ // mark as init element.attr('data-counter', 'init'); - var date = new Date( timestamp * 1000); + let date = new Date( timestamp * 1000); updateDateDiff(element, date); - var refreshIntervalId = window.setInterval(function(){ + // show element (if invisible) after first update + element.css({'visibility': 'initial'}); + + let refreshIntervalId = window.setInterval(function(){ // update element with current time if( !element.hasClass('stopCounter')){ diff --git a/js/app/init.js b/js/app/init.js index e99e6573..e067560d 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -6,7 +6,7 @@ define(['jquery'], function($) { 'use strict'; - var Config = { + let Config = { path: { img: 'public/img/', // path for images // user API @@ -22,6 +22,7 @@ define(['jquery'], function($) { searchAccess: 'api/access/search', // ajax URL - search user/corporation/ally by name // main config/map ping API initMap: 'api/map/init', // ajax URL - get static data + getAccessData: 'api/map/getAccessData', // ajax URL - get map access tokens (WebSocket) updateMapData: 'api/map/updateData', // ajax URL - main map update trigger updateUserData: 'api/map/updateUserData', // ajax URL - main map user data trigger // map API @@ -63,11 +64,35 @@ define(['jquery'], function($) { splashOverlay: 300, // "splash" loading overlay headerLink: 100, // links in head bar mapOverlay: 200, // show/hide duration for map overlays - mapMoveSystem: 300, // system position has changed animation + mapMoveSystem: 180, // system position has changed animation mapDeleteSystem: 200, // remove system from map mapModule: 200, // show/hide of an map module dialogEvents: 180 // dialog events /slide/show/... }, + syncStatus: { + type: 'ajax', + webSocket: { + status: 'closed', + class: 'txt-color-danger', + timestamp: undefined + }, + sharedWorker: { + status: 'offline', // SharedWorker status + class: 'txt-color-danger', + timestamp: undefined + }, + ajax: { + status: 'enabled', + class: 'txt-color-success', + timestamp: undefined + } + }, + performanceLogging: { + keyServerMapData: 'UPDATE_SERVER_MAP', // ajax request update map data + keyClientMapData: 'UPDATE_CLIENT_MAP', // update client map data + keyServerUserData: 'UPDATE_SERVER_USER_DATA', // ajax request update map user data + keyClientUserData: 'UPDATE_CLIENT_USER_DATA', // update client map user data + }, mapIcons: [ // map tab-icons { class: 'fa-desktop', diff --git a/js/app/logging.js b/js/app/logging.js index 252f668f..61f62e94 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -11,288 +11,312 @@ define([ 'use strict'; - var logData = []; // cache object for all log entries - var logDataTable = null; // "Datatables" Object + let logData = []; // cache object for all log entries + let logDataTable = null; // "Datatables" Object // Morris charts data - var maxGraphDataCount = 30; // max date entries for a graph - var chartData = {}; // chart Data object for all Morris Log graphs + let maxGraphDataCount = 30; // max date entries for a graph + let chartData = {}; // chart Data object for all Morris Log graphs - var config = { - dialogDynamicAreaClass: 'pf-dynamic-area', // class for dynamic areas - logGraphClass: 'pf-log-graph', // class for all log Morris graphs - tableToolsClass: 'pf-table-tools' // class for table tools + let config = { + taskDialogId: 'pf-task-dialog', // id for map "task manager" dialog + dialogDynamicAreaClass: 'pf-dynamic-area', // class for dynamic areas + timestampCounterClass: 'pf-timestamp-counter', // class for "timestamp" counter + taskDialogStatusAreaClass: 'pf-task-dialog-status', // class for "status" dynamic area + taskDialogLogTableAreaClass: 'pf-task-dialog-table', // class for "log table" dynamic area + logGraphClass: 'pf-log-graph', // class for all log Morris graphs + tableToolsClass: 'pf-table-tools' // class for table tools }; /** * get log time string * @returns {string} */ - var getLogTime = function(){ - - var serverTime = Util.getServerTime(); - - var logTime = serverTime.toLocaleTimeString('en-US', { hour12: false }); + let getLogTime = function(){ + let serverTime = Util.getServerTime(); + let logTime = serverTime.toLocaleTimeString('en-US', { hour12: false }); return logTime; }; + /** + * updated "sync status" dynamic dialog area + */ + let updateSyncStatus = function(){ + + // check if task manager dialog is open + let logDialog = $('#' + config.taskDialogId); + if(logDialog.length){ + // dialog is open + requirejs(['text!templates/modules/sync_status.html', 'mustache'], function(templateSyncStatus, Mustache) { + let data = { + timestampCounterClass: config.timestampCounterClass, + syncStatus: Init.syncStatus, + isWebSocket: () => { + return (Util.getSyncType() === 'webSocket'); + }, + isAjax: () => { + return (Util.getSyncType() === 'ajax'); + } + }; + + let syncStatusElement = $( Mustache.render(templateSyncStatus, data ) ); + + logDialog.find('.' + config.taskDialogStatusAreaClass).html( syncStatusElement ); + + logDialog.find('.' + config.timestampCounterClass).initTimestampCounter(); + + syncStatusElement.initTooltips({ + placement: 'right' + }); + }); + } + + }; + /** * shows the logging dialog */ - var showDialog = function(){ - + let showDialog = function(){ // dialog content - var content = $('
'); - - // content row for log graphs - var rowElementGraphs = $('
', { - class: 'row' - }); - - content.append(rowElementGraphs); - - - - var tableHeadline = $('

', { - text: ' Processes' - }).prepend( $('', { - class: ['fa', 'fa-fw', 'fa-lg', 'fa-microchip'].join(' ') - })); - - // add content Structure to dome before table initialization - content.append(tableHeadline); - - // log table area -------------------------------------------------- - var logTableArea = $('
', { - class: config.dialogDynamicAreaClass - }); - - var logTableActionBar = $('
', { - class: config.tableToolsClass - }); - logTableArea.append(logTableActionBar); - - var logTable = $('', { - class: ['compact', 'stripe', 'order-column', 'row-border'].join(' ') - }); - logTableArea.append(logTable); - - content.append(logTableArea); - - // init log table - logDataTable = logTable.DataTable({ - paging: true, - ordering: true, - order: [ 1, 'desc' ], - autoWidth: false, - hover: false, - pageLength: 15, - lengthMenu: [[10, 15, 25, 50, 50], [10, 15, 25, 50, 50]], - data: logData, // load cached logs (if available) - language: { - emptyTable: 'No entries', - zeroRecords: 'No entries found', - lengthMenu: 'Show _MENU_ entries', - info: 'Showing _START_ to _END_ of _TOTAL_ entries' - }, - columnDefs: [ - { - targets: 0, - title: '', - width: '18px', - searchable: false, - class: ['text-center'].join(' '), - data: 'status' - },{ - targets: 1, - title: '  ', - width: '50px', - searchable: true, - class: 'text-right', - data: 'time' - },{ - targets: 2, - title: '  ', - width: '35px', - searchable: false, - class: 'text-right', - sType: 'html', - data: 'duration' - },{ - targets: 3, - title: 'description', - searchable: true, - data: 'description' - },{ - targets: 4, - title: 'type', - width: '40px', - searchable: true, - class: ['text-center'].join(' '), - data: 'type' - },{ - targets: 5, - title: 'Prozess-ID   ', - width: '80px', - searchable: false, - class: 'text-right', - data: 'key' - } - ] - - }); - - // open dialog - var logDialog = bootbox.dialog({ - title: 'Task-Manager', - message: content, - size: 'large', - buttons: { - close: { - label: 'close', - className: 'btn-default' - } - } - }); - - // modal dialog is shown - logDialog.on('shown.bs.modal', function(e) { - - // show Morris graphs ---------------------------------------------------------- - - // function for chart label formation - var labelYFormat = function(y){ - return Math.round(y) + 'ms'; + requirejs(['text!templates/dialog/task_manager.html', 'mustache'], function(templateTaskManagerDialog, Mustache) { + let data = { + id: config.taskDialogId, + dialogDynamicAreaClass: config.dialogDynamicAreaClass, + taskDialogStatusAreaClass: config.taskDialogStatusAreaClass, + taskDialogLogTableAreaClass: config.taskDialogLogTableAreaClass, + tableActionBarClass: config.tableToolsClass }; - for(var key in chartData) { - if(chartData.hasOwnProperty(key)) { - // create a chart for each key + let contentTaskManager = $( Mustache.render(templateTaskManagerDialog, data) ); - var colElementGraph = $('
', { - class: ['col-md-6'].join(' ') - }); + let rowElementGraphs = contentTaskManager.find('.row'); + let taskDialogLogTableAreaElement = contentTaskManager.find('.' + config.taskDialogLogTableAreaClass); + let logTable = $('
', { + class: ['compact', 'stripe', 'order-column', 'row-border'].join(' ') + }); - // graph element - var graphElement = $('
', { - class: config.logGraphClass - }); + taskDialogLogTableAreaElement.append(logTable); - var graphArea = $('
', { - class: config.dialogDynamicAreaClass - }).append( graphElement ); - - // headline - var headline = $('

', { - text: key - }).prepend( - $('', { - class: ['txt-color', 'txt-color-grayLight'].join(' '), - text: 'Prozess-ID: ' - }) - ); - - // show update ping between function calls - var updateElement = $('', { - class: ['txt-color', 'txt-color-blue', 'pull-right'].join(' ') - }); - headline.append(updateElement).append('
'); - - // show average execution time - var averageElement = $('', { - class: 'pull-right' - }); - headline.append(averageElement); - - colElementGraph.append( headline ); - colElementGraph.append( graphArea ); - - graphArea.showLoadingAnimation(); - - rowElementGraphs.append( colElementGraph ); - - // cache DOM Elements that will be updated frequently - chartData[key].averageElement = averageElement; - chartData[key].updateElement = updateElement; - - chartData[key].graph = Morris.Area({ - element: graphElement, - data: [], - xkey: 'x', - ykeys: ['y'], - labels: [key], - units: 'ms', - parseTime: false, - ymin: 0, - yLabelFormat: labelYFormat, - padding: 10, - hideHover: true, - pointSize: 3, - lineColors: ['#375959'], - pointFillColors: ['#477372'], - pointStrokeColors: ['#313335'], - lineWidth: 2, - grid: false, - gridStrokeWidth: 0.3, - gridTextSize: 9, - gridTextFamily: 'Oxygen Bold', - gridTextColor: '#63676a', - behaveLikeLine: true, - goals: [], - goalLineColors: ['#66c84f'], - smooth: false, - fillOpacity: 0.3, - resize: true - }); - - graphArea.hideLoadingAnimation(); - - } - } - - // ------------------------------------------------------------------------------ - // add dataTable buttons (extension) - - var buttons = new $.fn.dataTable.Buttons( logDataTable, { - buttons: [ + // init log table + logDataTable = logTable.DataTable({ + paging: true, + ordering: true, + order: [ 1, 'desc' ], + autoWidth: false, + hover: false, + pageLength: 10, + lengthMenu: [[5, 10, 25, 50, 100, -1], [5, 10, 25, 50, 100, 'All']], + data: logData, // load cached logs (if available) + language: { + emptyTable: 'No entries', + zeroRecords: 'No entries found', + lengthMenu: 'Show _MENU_ entries', + info: 'Showing _START_ to _END_ of _TOTAL_ entries' + }, + columnDefs: [ { - extend: 'copy', - className: 'btn btn-sm btn-default', - text: ' copy' + targets: 0, + title: '', + width: '18px', + searchable: false, + class: ['text-center'].join(' '), + data: 'status' },{ - extend: 'csv', - className: 'btn btn-sm btn-default', - text: ' csv' + targets: 1, + title: '  ', + width: '50px', + searchable: true, + class: 'text-right', + data: 'time' + },{ + targets: 2, + title: '  ', + width: '35px', + searchable: false, + class: 'text-right', + sType: 'html', + data: 'duration' + },{ + targets: 3, + title: 'description', + searchable: true, + data: 'description' + },{ + targets: 4, + title: 'type', + width: '40px', + searchable: true, + class: ['text-center'].join(' '), + data: 'type' + },{ + targets: 5, + title: 'Prozess-ID   ', + width: '80px', + searchable: false, + class: 'text-right', + data: 'key' } ] - } ); - logDataTable.buttons().container().appendTo( $(this).find('.' + config.tableToolsClass)); - }); + }); - - // modal dialog is closed - logDialog.on('hidden.bs.modal', function(e) { - // clear memory -> destroy all charts - for (var key in chartData) { - if (chartData.hasOwnProperty(key)) { - chartData[key].graph = null; + // open dialog + let logDialog = bootbox.dialog({ + title: 'Task-Manager', + message: contentTaskManager, + size: 'large', + buttons: { + close: { + label: 'close', + className: 'btn-default' + } } - } - }); + }); - // modal dialog before hide - logDialog.on('hide.bs.modal', function(e) { + // modal dialog is shown + logDialog.on('shown.bs.modal', function(e) { + updateSyncStatus(); - // destroy logTable - logDataTable.destroy(true); - logDataTable= null; + // show Morris graphs ---------------------------------------------------------- + + // function for chart label formation + let labelYFormat = function(y){ + return Math.round(y) + 'ms'; + }; + + for(let key in chartData) { + if(chartData.hasOwnProperty(key)) { + // create a chart for each key + + let colElementGraph = $('
', { + class: ['col-md-6'].join(' ') + }); + + + // graph element + let graphElement = $('
', { + class: config.logGraphClass + }); + + let graphArea = $('
', { + class: config.dialogDynamicAreaClass + }).append( graphElement ); + + // headline + let headline = $('

', { + text: key + }).prepend( + $('', { + class: ['txt-color', 'txt-color-grayLight'].join(' '), + text: 'Prozess-ID: ' + }) + ); + + // show update ping between function calls + let updateElement = $('', { + class: ['txt-color', 'txt-color-blue', 'pull-right'].join(' ') + }); + headline.append(updateElement).append('
'); + + // show average execution time + let averageElement = $('', { + class: 'pull-right' + }); + headline.append(averageElement); + + colElementGraph.append( headline ); + colElementGraph.append( graphArea ); + + graphArea.showLoadingAnimation(); + + rowElementGraphs.append( colElementGraph ); + + // cache DOM Elements that will be updated frequently + chartData[key].averageElement = averageElement; + chartData[key].updateElement = updateElement; + + chartData[key].graph = Morris.Area({ + element: graphElement, + data: [], + xkey: 'x', + ykeys: ['y'], + labels: [key], + units: 'ms', + parseTime: false, + ymin: 0, + yLabelFormat: labelYFormat, + padding: 10, + hideHover: true, + pointSize: 3, + lineColors: ['#375959'], + pointFillColors: ['#477372'], + pointStrokeColors: ['#313335'], + lineWidth: 2, + grid: false, + gridStrokeWidth: 0.3, + gridTextSize: 9, + gridTextFamily: 'Oxygen Bold', + gridTextColor: '#63676a', + behaveLikeLine: true, + goals: [], + goalLineColors: ['#66c84f'], + smooth: false, + fillOpacity: 0.3, + resize: true + }); + + updateLogGraph(key); + + graphArea.hideLoadingAnimation(); + + } + } + + // ------------------------------------------------------------------------------ + // add dataTable buttons (extension) + + let buttons = new $.fn.dataTable.Buttons( logDataTable, { + buttons: [ + { + extend: 'copy', + className: 'btn btn-sm btn-default', + text: ' copy' + },{ + extend: 'csv', + className: 'btn btn-sm btn-default', + text: ' csv' + } + ] + } ); + + logDataTable.buttons().container().appendTo( $(this).find('.' + config.tableToolsClass)); + }); + + + // modal dialog is closed + logDialog.on('hidden.bs.modal', function(e) { + // clear memory -> destroy all charts + for (let key in chartData) { + if (chartData.hasOwnProperty(key)) { + chartData[key].graph = null; + } + } + }); + + // modal dialog before hide + logDialog.on('hide.bs.modal', function(e) { + + // destroy logTable + logDataTable.destroy(true); + logDataTable= null; + + // remove event -> prevent calling this multiple times + $(this).off('hide.bs.modal'); + }); - // remove event -> prevent calling this multiple times - $(this).off('hide.bs.modal'); }); }; @@ -300,9 +324,9 @@ define([ /** * updates the log graph for a log key * @param key - * @param duration + * @param duration (if undefined -> just update graph with current data) */ - var updateLogGraph = function(key, duration){ + let updateLogGraph = function(key, duration){ // check if graph data already exist if( !(chartData.hasOwnProperty(key))){ @@ -314,21 +338,23 @@ define([ } // add new value - chartData[key].data.unshift(duration); + if(duration !== undefined){ + chartData[key].data.unshift(duration); + } if(chartData[key].data.length > maxGraphDataCount){ chartData[key].data = chartData[key].data.slice(0, maxGraphDataCount); } function getGraphData(data) { - var tempChartData = { + let tempChartData = { data: [], dataSum: 0, average: 0 }; - for(var x = 0; x < maxGraphDataCount; x++){ - var value = 0; + for(let x = 0; x < maxGraphDataCount; x++){ + let value = 0; if(data[x]){ value = data[x]; tempChartData.dataSum = Number( (tempChartData.dataSum + value).toFixed(2) ); @@ -346,14 +372,14 @@ define([ return tempChartData; } - var tempChartData = getGraphData(chartData[key].data); + let tempChartData = getGraphData(chartData[key].data); // add new data to graph (Morris chart) - if is already initialized if(chartData[key].graph !== null){ - var avgElement = chartData[key].averageElement; - var updateElement = chartData[key].updateElement; + let avgElement = chartData[key].averageElement; + let updateElement = chartData[key].updateElement; - var delay = Util.getCurrentTriggerDelay( key, 0 ); + let delay = Util.getCurrentTriggerDelay( key, 0 ); if(delay){ updateElement[0].textContent = ' delay: ' + delay + 'ms '; @@ -365,8 +391,8 @@ define([ // change avg. display avgElement[0].textContent = 'Avg. ' + tempChartData.average + 'ms'; - var avgStatus = getLogStatusByDuration(key, tempChartData.average); - var avgStatusClass = Util.getLogInfo( avgStatus, 'class' ); + let avgStatus = getLogStatusByDuration(key, tempChartData.average); + let avgStatusClass = Util.getLogInfo( avgStatus, 'class' ); //change avg. display class if( !avgElement.hasClass(avgStatusClass) ){ @@ -376,7 +402,7 @@ define([ // change goals line color if(avgStatus === 'warning'){ chartData[key].graph.options.goalLineColors = ['#e28a0d']; - $(document).setProgramStatus('problem'); + $(document).setProgramStatus('slow connection'); }else{ chartData[key].graph.options.goalLineColors = ['#5cb85c']; } @@ -396,9 +422,8 @@ define([ * @param logDuration * @returns {string} */ - var getLogStatusByDuration = function(logKey, logDuration){ - - var logStatus = 'info'; + let getLogStatusByDuration = function(logKey, logDuration){ + let logStatus = 'info'; if( logDuration > Init.timer[logKey].EXECUTION_LIMIT ){ logStatus = 'warning'; } @@ -410,9 +435,9 @@ define([ * @param logType * @returns {string} */ - var getLogTypeIconClass = function(logType){ + let getLogTypeIconClass = function(logType){ - var logIconClass = ''; + let logIconClass = ''; switch(logType){ case 'client': @@ -427,11 +452,15 @@ define([ }; /** - * init logging -> set global log event + * init logging -> set global log events */ - var init = function(){ + let init = function(){ - var maxEntries = 150; + let maxEntries = 150; + + $(window).on('pf:syncStatus', function(){ + updateSyncStatus(); + }); // set global logging listener $(window).on('pf:log', function(e, logKey, options){ @@ -442,21 +471,19 @@ define([ options.duration && options.description ){ - var logDescription = options.description; - var logDuration = options.duration; - var logType = options.type; + let logDescription = options.description; + let logDuration = options.duration; + let logType = options.type; // check log status by duration - var logStatus = getLogStatusByDuration(logKey, logDuration); - - var statusClass = Util.getLogInfo( logStatus, 'class' ); - - var typeIconClass = getLogTypeIconClass(logType); + let logStatus = getLogStatusByDuration(logKey, logDuration); + let statusClass = Util.getLogInfo( logStatus, 'class' ); + let typeIconClass = getLogTypeIconClass(logType); // update graph data updateLogGraph(logKey, logDuration); - var logRowData = { + let logRowData = { status: '', time: getLogTime(), duration: '' + logDuration + 'ms', @@ -476,7 +503,7 @@ define([ } // delete old log entries from table --------------------------------- - var rowCount = logData.length; + let rowCount = logData.length; if( rowCount >= maxEntries ){ diff --git a/js/app/login.js b/js/app/login.js index 6c1ba6e3..1f044500 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -22,7 +22,7 @@ define([ 'use strict'; - var config = { + let config = { splashOverlayClass: 'pf-splash', // class for "splash" overlay // header @@ -75,11 +75,11 @@ define([ * @param cvalue * @param exdays */ - var setCookie = function(cname, cvalue, exdays) { - var d = new Date(); + let setCookie = function(cname, cvalue, exdays) { + let d = new Date(); d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = 'expires=' + d.toUTCString(); - var path = 'path=' + Util.getDocumentPath(); + let expires = 'expires=' + d.toUTCString(); + let path = 'path=' + Util.getDocumentPath(); document.cookie = cname + '=' + cvalue + '; ' + expires + '; ' + path; }; @@ -88,12 +88,12 @@ define([ * @param cname * @returns {*} */ - var getCookie = function(cname) { - var name = cname + '='; - var ca = document.cookie.split(';'); + let getCookie = function(cname) { + let name = cname + '='; + let ca = document.cookie.split(';'); - for(var i = 0; i ') + let newSlideContent = $('
') .addClass('text-content') .attr('imgTitle', obj.title); - var moduleConfig = { + let moduleConfig = { name: obj.href, // template name position: newSlideContent, functions: { @@ -185,7 +185,7 @@ define([ }; // render HTML file (template) - var moduleData = { + let moduleData = { id: config.headHeaderMapId, bgId: config.headMapBgId, neocomId: config.mapNeocomId, @@ -199,7 +199,7 @@ define([ }; // initialize carousel ------------------------------------------------ - var carousel = new Gallery([ + let carousel = new Gallery([ { imgTitle: 'Browser', href: 'ui/map', @@ -291,7 +291,7 @@ define([ * get all thumbnail elements * @returns {*|jQuery|HTMLElement} */ - var getThumbnailElements = function(){ + let getThumbnailElements = function(){ return $('a[data-gallery="#' + config.galleryId + '"]'); }; @@ -299,18 +299,18 @@ define([ * init gallery for thumbnail elements * @param newElements */ - var initGallery = function(newElements){ + let initGallery = function(newElements){ if( newElements.length > 0){ // We have to add ALL thumbnail elements to the gallery! // -> even those wthat are invisible (not lazyLoaded) now! // -> This is required for "swipe" through all images - var allThumbLinks = getThumbnailElements(); + let allThumbLinks = getThumbnailElements(); requirejs(['blueImpGalleryBootstrap'], function() { $(newElements).each(function() { - var borderless = false; + let borderless = false; - var galleryElement = $('#' + config.galleryId); + let galleryElement = $('#' + config.galleryId); galleryElement.data('useBootstrapModal', !borderless); galleryElement.toggleClass('blueimp-gallery-controls', borderless); @@ -318,10 +318,10 @@ define([ e.preventDefault(); e = e || window.event; - var target = e.target || e.srcElement; - var link = target.src ? target.parentNode : target; + let target = e.target || e.srcElement; + let link = target.src ? target.parentNode : target; - var options = { + let options = { index: link, event: e, container: '#' + config.galleryId, @@ -338,7 +338,7 @@ define([ /** * init "YouTube" video preview */ - var initYoutube = function(){ + let initYoutube = function(){ $('.youtube').each(function() { // Based on the YouTube ID, we can easily find the thumbnail image @@ -349,13 +349,13 @@ define([ $(document).delegate('#' + this.id, 'click', function() { // Create an iFrame with autoplay set to true - var iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1'; + let iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1'; if ( $(this).data('params') ){ iFrameUrl += '&'+$(this).data('params'); } // The height and width of the iFrame should be the same as parent - var iFrame = $('