* * PHPPE Core - Use as is */ /** * @file public/source.php * * @author bzt * @date 1 Jan 2016 * @brief PHPPE micro-framework's Core * * @see https://raw.githubusercontent.com/bztsrc/phppe3/master/public/source.php * * This is the nicely formatted and commented source version of PHPPE Core * * NOTE on unit testing: redirects and methods with die() cannot be tested * with PHPUnit. This does not mean they're untested, simply there are many * cases which are checked through PHPPE\Http::get() and not directly called * from test classes, see vendor/phppe/Developer/tests. Also note that * Developer extension ships with it's own PHPUnit compatible unit tester * class, so you can run the tests without phpunit.phar too. */ namespace PHPPE { define('VERSION', '3.0.0'); /** * Extension interface. We declare it as a class because * implementing event handlers are optional. */ class Extension { //function diag(){} //function init($cfg){} //function route($app,$action){} //function view($output){} //function filter(){} //function cronX($retCode){} //function stat() function __toString() { return get_class($this); } } /** * Add-On prototype. */ class AddOn { protected $name; //!< instance name public $args; //!< arguments in pharenthesis after type public $fld; //!< field name public $value; //!< object field's value protected $attrs; //!< attributes, everything after the name in tag protected $css; //!< css class to use, input or reqinput, occasionally errinput added protected $conf; //!< configuration scheme /** * Magic getter to implement read-only properties */ function __get($n) { return $this->$n; } /** * Constructor, do not try to override, use init() instead. * * @param array arguments, listed with pharenthesis after type in templates * @param string name of the add-on * @param mixed reference of object field * @param array attributes, listed after field name in templates * @param boolean required field flag * * @return PHPHE\AddOn instance */ final public function __construct($a, $n, &$v, $t = [], $r = 0) { //! save arguments, name and attributes $this->args = $a; $this->name = $n; $this->fld = strtr($n, ['.' => '_']); $this->value = $v; $this->attrs = $t; //! css class name reqinput for mandatory fields $this->css = ((!empty($r) ? 'req' : '').'input').(Core::isError($n) ? ' errinput' : ''); } /** * Init method is called when needed but only once per page generation * constructor may be called several times depending on the template. */ //! function init() //! { //! \PHPPE\Core::jslib() to load javascript libraries //! \PHPPE\Core::js() to add javascript functions and //! \PHPPE\Core::css() for style sheets here //! } /** * Field input or widget configuration form. * * @return string output */ //! function edit() {return "";} /** * Display a field's value or show widget face. * * @return string output */ public function show() { return htmlspecialchars($this->value); } /* * Value validator, returns boolean and a failure reason * * @param string name of value to valudate, for error reporting * @param mixed reference to value * @param array arguments * @param array attributes * @return [boolean,error message] if the first value is true, it's valid */ //! static function validate( $name, &$value,$args,$attrs ) //! { //! return [true, "Dummy validator that always pass"]; //! } function __toString() { return get_class($this)."(".$this->name.")"; } } /** * Filter prototype. */ class Filter { //static function filter() } /** * Client class, this is used to store client's information and session. */ class Client extends Extension { private $ip; //!< remote ip address. Also valid if behind proxy or load balancer private $agent; //!< client program private $user; //!< user account (unix user on CLI, http auth user on web) private $tz; //!< client's timezone public $lang; //!< client's prefered language public $screen = []; //!< screen dimensions public $geo = []; //!< geo location data (filled in by a third party extension) /** * Magic getter to implement read-only properties */ function __get($n) { return $this->$n; } /** * Constructor. Starts user session */ public function __construct($cfg=[]) { Core::$client = $this; //! start user session if(empty($_SESSION)){ // no way we could test this as session is already started when unit tests reach this // @codeCoverageIgnoreStart @session_name(!empty(Core::$core->sessionvar) ? Core::$core->sessionvar : 'pe_sid'); @session_start(); // @codeCoverageIgnoreEnd } //! refresh session cookie if (ini_get('session.use_cookies')) { @setcookie(session_name(), session_id(), Core::$core->now + Core::$core->timeout, '/', Core::$core->base, !empty(Core::$core->secsession) ? 1 : 0, 1); } //! destroy user session if requested if (isset($_REQUEST['clear'])) { // @codeCoverageIgnoreStart //! save logged in user if any $d = 'pe_u'; $u = @$_SESSION[$d]; //! keep user object, but clear preferences $u->data=[]; $_SESSION = []; $_SESSION[$d] = $u; //! redirect user to reload everything Http::redirect(); // @codeCoverageIgnoreEnd } //! override autodetected timezone with a fixed one if(!empty($cfg['tz'])) { // @codeCoverageIgnoreStart $this->tz = $cfg['tz']; // @codeCoverageIgnoreEnd } } /** * Initialize event. Collects information on client (language, timezone, screen size etc.) * * @param array configuration * * @return boolean false if initialization failed */ public function init($cfg = []) { Core::$l = []; View::assign('client', $this); //! set up client's prefered language $L = 'pe_l'; $a = ''; $d = []; //! get prefered language from browser or from environment if (empty($_SESSION[$L])) { //! user preference if(!empty($_SESSION['pe_u']->data['lang'])) $d=[$_SESSION['pe_u']->data['lang']]; else { $i = 'HTTP_ACCEPT_LANGUAGE'; $d = explode(',', strtr(!empty($_SERVER[$i]) ? $_SERVER[$i] : (getenv('LANG') || 'en'), ['/' => ''])); } } //! this can be overriden from url if (!empty($_REQUEST['lang'])) { $d = [strtr(trim($_REQUEST['lang']), ['/' => ''])]; } //! look for valid language code //! only allow if language is defined in core or in app foreach ($d as $v) { list($a) = explode(';', strtolower(str_replace('-', '_', $v))); if (!empty($a) && (!empty(glob("vendor/phppe/*/lang/$a.php",GLOB_NOSORT)) || $a == 'en')) { $_SESSION[$L] = $a; break; } } //! failsafe if (empty($_SESSION[$L])) { $_SESSION[$L] = 'en'; } $this->lang = $v = $_SESSION[$L]; $i = explode('_', $v); //! set PHP locale for the language setlocale(LC_ALL, strtolower($i[0]).'_'.strtoupper(!empty($i[1]) ? $i[1] : $i[0]).'.UTF8'); //! load dictionary for core Core::lang('Core'); Core::lang('app'); $L = 'pe_tz'; if (Core::$w) { //! Detect values for Web // @codeCoverageIgnoreStart $d = 'HTTP_USER_AGENT'; $c = empty($_REQUEST['cache']) && !(empty($_SERVER[$d])||$_SERVER[$d]=="API"|| strpos(strtolower($_SERVER[$d]),"wget")!==false|| strpos(strtolower($_SERVER[$d]),"curl")!==false); if (!isset($_REQUEST['nojs']) && empty($_SESSION[$L]) && $c) { //! this is a small JavaScript page that shows up for the first time //! after collecting information it redirects user so fast, he won't //! notice a thing. if (empty($_REQUEST['n'])) { $_SESSION['pe_n'] = sha1(rand()); //! save redirection url Http::_r();$u=$_SESSION['pe_r'].(strpos($_SERVER['REQUEST_URI'], '?') === false ? '?' : '&'); $g = 'getTimezoneOffset()'; $d = "var d%=new Date();d%.setDate(1);d%.setMonth(@);d%=parseInt(d%.$g);"; die(""); } elseif ($_REQUEST['n'] == $_SESSION['pe_n']) { unset($_SESSION['pe_n']); $_SESSION[$L] = timezone_name_from_abbr('', $_REQUEST['t'] + 0, $_REQUEST['d'] + 0); $_SESSION['pe_w'] = floor($_REQUEST['w']); $_SESSION['pe_h'] = floor($_REQUEST['h']); $_SESSION['pe_j'] = true; Http::redirect(); } } if (isset($_REQUEST['nojs'])||isset($_REQUEST['n'])||!empty($_SESSION['pe_n'])) { if($c && empty($_COOKIE)){$t=View::template("nocookies");die($t?$t:L("Please enable cookies"));} unset($_SESSION['pe_n']); $_SESSION['pe_j']=false; $_SESSION[$L]="UTC"; if(!isset($_REQUEST['nojs']))Http::redirect(); } // @codeCoverageIgnoreEnd //! get client's real ip address $d = 'HTTP_X_FORWARDED_FOR'; $this->ip = $i = !empty($_SERVER[$d]) ? $_SERVER[$d] : $_SERVER['REMOTE_ADDR']; $this->screen = !empty($_SESSION['pe_w']) ? [$_SESSION['pe_w'], $_SESSION['pe_h']] : [0, 0]; //! agent is user agent $d = 'HTTP_USER_AGENT'; $this->agent = !empty($_SERVER[$d]) ? $_SERVER[$d] : 'browser'; //! user is http auth user $d = 'PHP_AUTH_USER'; $this->user = !empty($_SERVER[$d]) ? $_SERVER[$d] : ''; $this->js = intval(@$_SESSION['pe_j']); } else { //! detect values for CLI $T = getenv('TZ'); //! this should be a symlink to something //! like /usr/share/zoneinfo/Europe/London $d = explode('/', $T ? $T : @readlink('/etc/localtime')); $c = count($d); $_SESSION[$L] = $c > 1 ? $d[$c - 2].'/'.$d[$c - 1] : 'UTC'; //! no IP for tty $this->ip = 'CLI'; //! query tty size. If you know a better, exec()-less way, let me know!!! $c = intval(exec('tput cols 2>/dev/null')); $d = intval(exec('tput lines 2>/dev/null')); $this->screen = [$c < 1 ? 80 : $c, $d < 1 ? 25 : $d]; //! agent is a terminal $d = getenv('TERM'); $this->agent = !empty($d) ? $d : 'term'; //! user is standard unix user $d = getenv('USER'); $this->user = !empty($d) ? $d : ''; $this->js = false; Core::$core->noframe = 1; } //! set up client's timezone if(empty($this->tz)) { $this->tz = !empty($_SESSION[$L]) ? $_SESSION[$L] : 'UTC'; } date_default_timezone_set($this->tz); } } /** * Model that supports Object Relational Mapping. */ class Model { public $id; public $name; //! this breaks PSR-2, but required to exclude table name from sql protected static $_table; /** * Default model constructor. Load object with data if id given. Id can be * a property value list (associative array or object) as well. * * @param integer/mixed id/properties */ function __construct($id="") { if(!empty($id)) { if(is_scalar($id)) { $this->id = $id; $this->load($id); } else { $d=get_object_vars($this); foreach($id as $k=>$v) { if($k[0]!="_" && array_key_exists($k,$d)) $this->$k=$v; } } } } /** * Find objects of the same kind in database. * * @param string/array search phrase(s) * @param string where clause with placeholders * @param string order by * @param string fields, comma separated list * * @return array of associative arrays */ public static function find($s = [], $w = '', $o = '', $f = '*', $g = '') { if (empty(static::$_table)) { throw new \Exception('no _table'); } //get the records from current datasource return DS::query($f, static::$_table, !empty($w) ? $w : (!empty($s)?'id=?':''), !empty($g) ? $g : '', $o, 0, 0, is_array($s) ? $s : [$s]); } /** * Load, reload or find a record in database and load result into this object. * * @param integer id of the object to load, or (if second argument given) search phrase * @param string where clause with placeholders * @param string order by (optional) * * @return true on success */ public function load($i = 0, $w = '', $o = '') { if (empty(static::$_table)) { throw new \Exception('no _table'); } //get the record from current datasource $r = (array)DS::fetch('*', static::$_table, $w ? $w : 'id=?', '', $o, is_array($i) ? $i : [$i ? $i : $this->id]); //update property values. FETCH_INTO not exactly what we want if ($r) { foreach (get_object_vars($this) as $k => $v) { if ($k[0] != '_') { $this->$k = is_string($r[$k]) && !empty($r[$k]) && ($r[$k][0] == '{' || $r[$k][0] == '[') ? json_decode($r[$k], true) : $r[$k]; } } return true; } return false; } /** * Save the current object into database. May also alter $id property (and that only). * * @param boolean force insert * * @return boolean true on success */ public function save($f = 0) { if (empty(static::$_table)) { throw new \Exception('no _table'); } $d = DS::db(); if (empty($d)) { throw new \Exception('no ds'); } //build the arguments array $a = []; foreach (get_object_vars($this) as $k => $v) { if ($k[0] != '_' && ($f || $k != 'id') && ($k != 'created'||!empty($v)) ) { $a[$k] = is_scalar($v) ? $v : json_encode($v); } } if (!DS::exec(($this->id && !$f ? 'UPDATE '.static::$_table.' SET '.implode('=?,', array_keys($a)).'=? WHERE id='.$d->quote($this->id) : 'INSERT INTO '.static::$_table.' ('.implode(',', array_keys($a)).') VALUES (?'.str_repeat(',?', count($a) - 1).')'), array_values($a))) { return false; } //save new id for inserts if (!$this->id || $f) { $this->id = $d->lastInsertId(); } //return id return $this->id; } } /** * Default user class, will be extended by PHPPE Pack with Users class. */ class User extends Model { public $id = 0; //!< only for Anonymous. Otherwise user id can be a string as well public $name = 'Anonymous'; //!< user real name public $data = []; //!< user preferences // protected static $_table = "users"; //! set table name. This should be in Users class! protected $acl = []; //!< Access Control List // private remote = []; //!< remote server configuration, added run-time /** * Check access for an access control entry. * * @param string/array access control entry or list (pipe separated string or array) * * @return boolean true or false */ final public function has($l) { //check if at least one of the ACE match foreach (is_array($l) ? $l : explode('|', $l) as $a) { $a = trim($a); //is user logged in? //is superadmin with bypass priviledge? if (!empty($this->id) && ($this->id == -1 || $a == 'loggedin' || !empty($this->acl[$a]) || !empty($this->acl["$a:".Core::$core->item]))) { return true; } } return false; } /** * Grant priviledge for a user. * * @param string/array access control entry or list (pipe separated string or array) */ public function grant($l) { foreach (is_array($l) ? $l : explode('|', $l) as $a) { $a = trim($a); if (!empty($this->id) && !empty($a)) { $this->acl[$a] = true; } } } /** * Drop privileges, specific access control entry or the whole access control list. * * @param string/array access control entry or list (pipe separated string or array) or empty for dropping all */ public function clear($l = '') { if (empty($l)) { $this->acl = []; } else { foreach (is_array($l) ? $l : explode('|', $l) as $a) { $a = trim($a); //! drop the ACE unset($this->acl[$a]); //! drop item specific ACEs as well foreach ($this->acl as $k => $v) { if (substr($k, 0, strlen($a) + 1) == $a.':') { unset($this->acl[$k]); } } } } } /** * Initialize event. * * @param array configuration array * * @return boolean false if initialization failed */ public function init($cfg) { $L = 'pe_u'; if (!empty($_SESSION[$L]) && is_object($_SESSION[$L])) { Core::$user = $_SESSION[$L]; foreach (['id', 'name', 'data'] as $k) { $this->$k = Core::$user->$k; } } else { Core::$user = $_SESSION[$L] = $this; } View::assign('user', Core::$user); } /** * Route event. We handle login/logout actions here, before routing * decision is made. * * @param current application * @param current action */ // @codeCoverageIgnoreStart public function route($app, $action) { //! operation modes if (!empty(Core::$user->id)) { //! edit for updating records and conf for widget configuration foreach (['edit', 'conf'] as $v) { if (isset($_REQUEST[$v]) && Core::$user->has($v)) { $_SESSION['pe_'.substr($v, 0, 1)] = !empty($_REQUEST[$v]); Http::redirect(); } } } //! handle hardwired admin login and logout before Users class get's a chance if (Core::$core->app == 'login') { //! if already logged in redirect to home page if (Core::$user->id) { Http::redirect('/'); } //! superuser's name $A = 'admin'; if (Core::isBtn() && !empty($_REQUEST['id'])) { Core::req2arr("login"); if(!Core::isError()) { foreach($_SESSION as $k=>$v) if(substr($k,0,3)!="pe_") unset($_SESSION[$k]); //don't accept password in GET parameter if ($_REQUEST['id'] == $A && !empty(Core::$core->masterpasswd) && password_verify($_POST['pass'], Core::$core->masterpasswd)) { $_SESSION['pe_u']->id = -1; $_SESSION['pe_u']->name = $A; } else { //! *** LOGIN Event *** Core::event("login", [$_REQUEST['id'],$_POST['pass']]); } if(!empty($_SESSION['pe_u']->id)) { Core::log('A', 'Login '.$_SESSION['pe_u']->name, 'users'); Http::redirect(); } else { Core::error(L('Bad username or password'), 'id'); } } } } elseif (Core::$core->app == 'logout') { $i = Core::$user->id; if ($i) { Core::log('A', 'Logout '.Core::$user->name, 'users'); //! hook Users class' method for non-admin user logouts if ($i != -1) { //! *** LOGOUT Event *** Core::event("logout"); } } session_destroy(); Http::redirect('/'); } } // @codeCoverageIgnoreEnd } /** * HTTP helpers. */ class Http { private static $r; //!< url routes /** * Generate a permanent link (see also url()). * * @param string application * @param string action */ public static function url($m = '', $p = '') { //! generate canonized permanent link $c = Core::$core->base; $A = Core::$core->app; $f = basename(__FILE__); if (empty($m) && !empty($A)) { $m = $A; } if (empty($p) && !empty($A) && $A == $m) { $p = Core::$core->action; } $a = ($m != '/' ? ($m.$p != 'indexaction' ? $m.'/' : '').(!empty($p) && $p != 'action' ? $p.'/' : '') : ''); return 'http'.(Core::$core->sec ? 's' : '').'://'.$c.($c[strlen($c) - 1] != '/' ? '/' : ''). ($f != 'index.php' ? $f.($a?'/':'') : '').$a; } /** * Redirect user. * * @param string url to redirect to * @param boolean save current url before redirect so that it will be used next time */ // @codeCoverageIgnoreStart public static function redirect($u = '', $s = 0) { //save current url if ($s) { self::_r(); } //get redirection url if exists if (empty($u) && !empty($_SESSION['pe_r'])) { $u = $_SESSION['pe_r']; unset($_SESSION['pe_r']); } //redirect user header('HTTP/1.1 302 Found'); $f = basename(__FILE__); header('Location:'.(!empty($u) ? (strpos($u, '://') ? $u : 'http'.(Core::$core->sec ? 's' : '').'://'. Core::$core->base.($f != 'index.php' ? $f.'/' : '').($u != '/' ? $u : '')) : self::url().Core::$core->item)); exit(); } // @codeCoverageIgnoreEnd /** * Application allowed to call this in special cases, but normally won't need it. * This function saves current request uri in session for later redirection. */ public static function _r() { //! save request uri, will be used later after successful login //! called when redirect has true as second argument. @$_SESSION['pe_r'] = 'http'.(Core::$core->sec ? 's' : '').'://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; } /** * Generate http header with mime info for content. * * @param string mime type of output * @param boolean client side cache enabled */ public static function mime($m, $c = true) { //on cli this will most probably report an error //as usually cli action handlers have already echoed output by now if ($c && empty(Core::$core->nocache)) { @header('Pragma:cache'); @header('Cache-Control:cache,public,max-age='.Core::$core->cachettl); @header('Connection:close'); } else { @header('Pragma:no-cache'); @header('Cache-Control:no-cache,no-store,private,must-revalidate,max-age=0'); } @header("Content-Type:$m;charset=utf-8"); } /** * Query routing table. * @usage route() * @return array of routing rules * * Register a new url route. This method can handle many different input formats * @usage route(...) call it from your initialization code, extension/init.php * @param regexp mask of url * @param class in which the app resides * @param method of the application action handler (if not given, default action routing applies) * @param filters comma separated list or array (ACE has to be started with '@' or PHPPE\Filter\*::filter() will be used) */ public static function route($u = '', $n = '', $a = '', $f = []) { if (empty($u)) { return self::$r; } $A = 'action'; $F = 'filters'; $U = 'url'; $N = 'name'; //! standard arguments if (is_string($u) && !empty($n)) { if (!is_array($f)) { $f = str_getcsv($f, ','); } self::$r[self::h($u, $n, $a)] = [$u, $n, $a, $f]; } //! associative array elseif (is_array($u) && !empty($u[$U]) && !empty($u[$N])) { $f = !empty($u[$F]) ? $u[$F] : []; $a = !empty($u[$A]) ? $u[$A] : ''; self::$r[self::h($u[$U], $u[$N], $a)] = [$u[$U], $u[$N], $a, is_array($f) ? $f : explode(',', $f)]; } //! mass import from an array elseif (is_array($u) && !empty(current($u)[0])) { foreach ($u as $v) { self::$r[self::h($v[0], $v[1], (!empty($v[2]) ? $v[2] : ''))] = $v; } } //! from stdClass elseif (is_object($u) && !empty($u->$U) && !empty($u->$N)) { $f = !empty($u->$F) ? $u->$F : []; $a = !empty($u->$A) ? $u->$A : ''; self::$r[self::h($u->$U, $u->$N, $a)] = [$u->$U, $u->$N, $a, is_array($f) ? $f : explode(',', $f)]; } else { throw new \Exception('bad route: '.serialize($u)); } //! limit check // @codeCoverageIgnoreStart if (count(self::$r) >= 512) { Core::log('C', 'too many routes'); } // @codeCoverageIgnoreEnd } /** * Get controller for an url. * * @param string application * @param string action * @param string url * * @return [application,action,arguments] */ public static function urlMatch($app = '', $ac = '', $url = '') { $X = []; if (empty($app)) { //! url routing $w = 0; if (!empty(self::$r)) { //! check routes, best match policy uasort(self::$r, function ($a, $b) { return strcmp($b[0], $a[0]); }); //! check route patterns foreach (self::$r as $v) { //! if matches current url if (preg_match('!^'.strtr($v[0], ['!' => '']).'!i', $url, $X)) { //! check filter if (!empty($v[3]) && !Core::cf($v[3])) { $w = 1; continue; } $w = 0; //! chop off whole match (first index) from arguments array_shift($X); //! get class and method $app = $v[1]; $ac = $v[2]; break; } } } //! if there was a match but failed due to filters, //! set output to 403 Access Denied page if ($w) { $app = Core::$core->template = '403'; } } //! load detected values if no application given if (empty($app)) $app = Core::$core->app; if (empty($ac)) $ac = Core::$core->action; return [$app, $ac, $X]; } /** * make a http request and return content. Unlike cURL, this will follow cookie changes during redirects. * * @param string url * @param array post variables * @param integer timeout in sec * * @return content */ public static function get($u, $p = '', $T = 3, $l = 0) { static $C; //! check recursion maximum level if ($l > 7) { return; } //! parse url if (preg_match("/^([^\:]+)\:\/\/([^\/\:]+)\:?([0-9]*)(.*)$/", $u, $m)) { //! validation and default values $s = 0; if ($m[1] != 'http' && $m[1] != 'https') return; if ($m[1] == 'https') { $s = 1; $m[2] = 'ssl://'.$m[2]; } if ($m[3] == '') $m[3] = ($m[1] == 'http' ? 80 : 443); if ($m[4] == '') $m[4] = '/'; //! open socket $f = fsockopen($m[2], $m[3], $n, $e, $T); if (!$f) { // @codeCoverageIgnoreStart //log failure Core::log('E', "$u #$n $e", 'http'); //give it a fallback in case ssl transport not configured in php return $s && strpos($e, '"ssl"') ? file_get_contents($u, false, is_array($p) ? stream_context_create([ 'http' => ['method' => 'POST', 'header' => 'Content-type: application/x-www-form-urlencoded', 'content' => http_build_query($p),],]) : null) : ''; // @codeCoverageIgnoreEnd } //! construct POST $P = is_array($p) ? http_build_query($p, '_') : ''; //! send request //! we are using HTTP/1.0 on purpose so that we don't have to mess with chunked response $o = ($P ? 'POST' : 'GET').' '.$m[4]." HTTP/1.0\r\nHost: ".$m[2]."\r\nAccept-Language: ".Core::$client->lang.";q=0.8\r\n".($C ? 'Cookie: '.http_build_query($C, '', ';')."\r\n" : '').($P ? "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($P)."\r\n" : '')."Connection: close;\r\n\r\n".$P; fwrite($f, $o); //! receive response $d = $H = $n = ''; $h = '-'; $t = 0; stream_set_timeout($f, $T); while (!feof($f) && trim($h) != '') { //! parse headers $h = trim((fgets($f, 4096))); if (!empty($h)) { $H = strtolower($h); if (substr($H, 0, 8) == 'location') { $n = trim(substr($h, 9)); } if (substr($H, 0, 12) == 'content-type' && strpos($h, 'text/')) { $t = 1; } //! follow cookie changes if (substr($H, 0, 10) == 'set-cookie') { $c = str_getcsv(str_getcsv(trim(substr($h, 11)),';')[0], '='); //c[1] is undefined on nginx when clearing the cookie @$C[$c[0]] = $c[1]; } } } //! handle redirections if ($n && $n != $u) { return self::get($n, $p, $T, $l + 1); } //! receive data if there was a header (not timed out) if ($H) { while (!feof($f)) { $d .= fread($f, 65535); } Core::log('D', "$u ".strlen($d), 'http'); } // @codeCoverageIgnoreStart else { Core::log('E', "$u timed out $T", 'http'); } // @codeCoverageIgnoreEnd fclose($f); return $t ? strtr($d, ["\r" => '']) : $d; } } /** * Calculate hash for routes and others */ private static function h($a, $b, $c = '') { return sha1($a.'|'.$b.'|'.$c); } } /** * DataSource layer. It's called DS and not DB because class DB is * the Sql Query Builder shipped with PHPPE Pack. */ class DS extends Extension { private $name = ''; private static $db = []; //!< database layer private static $s = 0; //!< data source selector private static $b = 0; //!< time consumed by data source queries (bill for db) /** * Magic getter to implement read-only properties */ function __get($n) { return $this->$n; } /** * Constructor. Initialize primary datasource if any. Called by core. * * @param string primary datasource uri */ public function __construct($ds = null) { //! initialize primary datasource if configured prior module initialization if (!empty($ds)) { //! replace string $this->db with an array of pdo objects @self::db($ds); //get primary datasource's name if (!empty(self::$db[0])) { $this->name = self::$db[0]->name; //! get current timestamp from primary datasoure //! this will override time() in $core->now with //! a time in database server's timezone. This is //! important if webserver and dbserver are on //! separate hosts without time synchronization. try { $t = @strtotime(@self::field('CURRENT_TIMESTAMP')); if ($t > 0) { Core::$core->now = $t; } // @codeCoverageIgnoreStart } catch (\Exception $e) { } } // @codeCoverageIgnoreEnd } } /** * Close database connections for all datasources. */ public static function close() { if (!empty(self::$db)) { foreach (self::$db as $d) { if (method_exists($d, 'close')) { // @codeCoverageIgnoreStart $d->close(); // @codeCoverageIgnoreEnd } } } self::$db = []; self::$s = 0; } /** * Diag event handler. Look for sql updates. */ public function diag() { //! nothing to do without database $d = @self::$db[0]; if (empty($d)) { return; } //! apply sql updates $D = []; foreach (['', '.'.$d->name] as $s) { $D += array_fill_keys(@glob('vendor/phppe/*/sql/upd_*'.$s.'.sql',GLOB_NOSORT), 0); } if (!empty($D)) { echo "DIAG-I: db update (".count($D).")\n"; } foreach ($D as $f => $v) { //! get sql commands from file $s = str_getcsv(file_get_contents($f), ';'); @unlink($f); //! execute one by one foreach ($s as $q) { self::exec($q); } } } /** * return database instance for current selector * @usage DS::db() * * initialize a database and make connection available as a data source * @usage DS::db(pdodsn) * * @param string pdo dsn of new connection * @param pdo optional: any PDO compatible class instance * * @return pdo/integer pdo instance for query or selector for this new data source */ public static function db($u = null, $O = null) { //! query PDO instance if (empty($u)) { return !empty(self::$db[self::$s]) ? self::$db[self::$s] : null; } //! initialize a database and make connection available as a data source $S = microtime(1); //! create instance try { //! get username and password if it's not part of dsn if (!preg_match('/^(.*)@([^@:]+)?:?([^:]*)$/', $u, $d)) { $d[1] = $u; } self::$s = count(self::$db); self::$db[] = is_object($O) ? $O : new \PDO($d[1], !empty($d[2]) ? $d[2] : '', !empty($d[3]) ? $d[3] : '', [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_EMULATE_PREPARES => 0]); if (!isset(self::$db[self::$s])) throw new \PDOException(); //! housekeeping $d = &self::$db[self::$s]; $d->id = count(self::$db); $d->name = is_object($O) ? get_class($O) : $d->getAttribute(\PDO::ATTR_DRIVER_NAME); //! to maintain interoperability among different sql implementations, load replacements from //! vendor/phppe/*/libs/ds_(driver).php $d->s = @include @glob('vendor/phppe/*/libs/ds_'.$d->name.'.php')[0]; if (!empty($d->s['_init'])) { // @codeCoverageIgnoreStart //! driver specific commands for connection $c = str_getcsv($d->s['_init'], ';'); foreach ($c as $n => $C) { if (!empty(trim($C))) { $d->exec(trim($C)); } } // @codeCoverageIgnoreEnd } } catch (\Exception $e) { //! consider failure of first data source fatal Core::log(self::$s ? 'E' : 'C', L('Unable to initialize').": $u, ".$e->getMessage(), 'db'); throw $e; } self::$b += microtime(1) - $S; //! return selector of newly created instance return self::$s; } /** * Set current data source to use with exec, fetch etc. if argument given. * * @param integer data source selector (returned by db()) * * @return integer returns current selector */ public static function ds($s = -1) { //! select a data source to use if ($s >= 0 && $s < count(self::$db) && !empty(self::$db[$s])) { self::$s = $s; } return self::$s; } /** * Convert a string from user into a sql like phrase. * * @param string user profided string * * @return string sql safe, formatted string */ public static function like($s) { return preg_replace('/[%_]+/', '%', '%'. preg_replace("/[^a-z0-9\%]/i", '_', preg_replace("/[\ \t]+/", '%', strtr(trim($s), ['%' => '']))).'%'); } /** * Common code for executing a query on current data source. All the other methods are wrappers only. * * @param string query string * * @return array/integer number of affected rows or data array */ public static function exec($q, $a = []) { //! log query in developer mode Core::log('D', $q.' '.json_encode($a), 'db'); //! check for valid datasource if (!is_array($a)) { $a = [$a]; } if (empty(self::$db[self::$s])) { throw new \Exception(L('Invalid ds').' #'.self::$s); } //! skip comment lines and empty queries by //! reporting 1 affected row to avoid errors on caller side $q = trim($q); if (empty($q) || $q[0] == '-' || $q[0] == '/') { return 1; } //! do the thing $t = microtime(1); $r = null; $h = self::$db[self::$s]; try { $i = strtolower(substr(trim($q), 0, 6)) == 'select' || strtolower(substr(trim($q), 0, 4)) == 'show'; //! to maintain interoperability among different sql implementations, a replace //! array is used with regexp pattern keys and replacement strings as value //! see db() it's initialized there. The array is specified here: //! vendor/phppe/*/libs/ds_(driver).php if (is_array($h->s)) { foreach ($h->s as $k => $v) { if ($k[0] != '_') { $q = preg_replace('/'.$k.'/ims', $v, $q); } } if(!$i && strtolower(substr(trim($q), 0, 6)) == 'select') $i=1; } //! prepare and execute the statement with arguments $s = $h->prepare($q); @$s->execute($a); //! get result, either an array or a number $r = $i ? $s->fetchAll(\PDO::FETCH_ASSOC) : $s->rowCount(); } catch (\Exception $e) { //! try to load scheme for missing table $E = $e->getMessage(); $c = strtr($E, 'le or v', ''); // @codeCoverageIgnoreStart if ((/* other */(!empty($h->s['_tablename']) && preg_match($h->s['_tablename'], $E, $d)) || /*Sqlite/MySQL/MariaDB*/ preg_match("/able:?\ [\'\"]?([a-z0-9_\.]+)/mi", $c, $d) || /*Postgre*/ preg_match("/([a-z0-9_\.]+)[\'\"] does\ ?n/mi", $E, $d) || /*MSSql*/ preg_match("/name:?\ [\'\"]?([a-z0-9_\.]+)/mi", $E, $d) ) && !empty($d[1])) { // @codeCoverageIgnoreEnd $c = ''; $m = '.'.trim($h->name); $d = explode('.', $d[1]); $d = trim(!empty($d[1]) ? $d[1] : $d[0]); list($D) = explode('_', $d); //! look for engine specific scheme $f = @glob('vendor/phppe/*/sql/'.$d.$m.'.sql',GLOB_NOSORT)[0]; //! common scheme if (empty($f)) { $f = @glob('vendor/phppe/*/sql/'.$d.'.sql',GLOB_NOSORT)[0]; } if (!empty($f) && file_exists($f)) { $c = file_get_contents($f); } //! if scheme not found. Only log pages and views missing in debug runlevel if (empty($c)) { if(($d!="pages"&&$d!="views") || Core::$core->runlevel > 1) Core::log('E', $E, 'db'); throw $e; } if (is_array($h->s)) { foreach ($h->s as $k => $v) { if ($k[0] != '_') { $c = preg_replace('/'.$k.'/ims', $v, $c); } } } //! execute schema creation commands $c = str_getcsv($c, ';'); foreach ($c as $n => $C) { try { if (!empty(trim($C))) { $h->exec(trim($C)); } } catch (\Exception $e) { Core::log('E', "creating $d at line:".($n + 1).' '.$e->getMessage(), 'db'); throw $e; } } Core::log('A', "$d created.", 'db'); //! repeat original command $s = $h->prepare($q); $s->execute($a); $r = $i ? $s->fetchAll(\PDO::FETCH_ASSOC) : $s->rowCount(); } else { Core::log('E', $q.' '.json_encode($a).' '.$E, 'db'); $r = null; throw $e; } } //! housekeeping self::$b += microtime(1) - $t; return $r; } /** * Query records from current data source. * * @param string fields * @param string table * @param string where clause * @param string group by * @param string order by * @param integer offset * @param integer limit * @param array arguments * * @return array/integer */ public static function query($f, $t, $w = '', $g = '', $o = '', $s = 0, $l = 0, $a = []) { //! execute a query that returns records of associative arrays $q = 'SELECT '.(is_array($f)?implode(",",$f):$f).($t ? ' FROM '.$t : '').($w ? ' WHERE '.$w : '').($g ? ' GROUP BY '.$g : '').($o ? ' ORDER BY '.$o : '').($l ? (' LIMIT '.($s ? $s.',' : '').$l) : '').';'; return self::exec($q, $a); } /** * Query one record from current data source. * * @param string fields * @param string table * @param string where clause * @param string group by * @param string order by * @param array arguments * * @return array record */ public static function fetch($f, $t = '', $w = '', $g = '', $o = '', $a = []) { //! return the first record $r = self::query($f, $t, $w, $g, $o, 0, 1, $a); return (object)(empty($r[0]) ? [] : $r[0]); } /** * Query one field from current data source. * * @param string field * @param string table * @param string where clause * @param string group by * @param string order by * @param array arguments * * @return string value */ public static function field($f, $t = '', $w = '', $g = '', $o = '', $a = []) { //! return the first field return @reset(self::fetch($f, $t, $w, $g, $o, $a)); } /** * Query a recursive tree from current data source. * * @param string query string, use '?' placeholder to mark place of parent id * @param integer root id of the tree, 0 for all * * @return array of data */ public static function tree($q, $p = 0) { //! return a tree array (childs in _) $r = self::exec($q, [$p]); if (empty($r)) { return[]; } foreach ($r as $k => $v) { $i = isset($v['id']) ? $v['id'] : -1; if (!empty($i) && $i != $p) { $c = self::tree($q, $i); if (!empty($c)) { $r[$k]['_'] = $c; } } } return $r; } /** * Return time consumed by database calls. * * @return integer secs */ public static function bill() { return self::$b; } } /** * Cache wrapper. Allow multiple options * and fallbacks to php memcache. */ class Cache extends Extension { private $name; //!< implementation private static $uri; //!< cache uri public static $mc; //!< memcache instance /** * Magic getter to implement read-only properties */ function __get($n) { return $this->$n; } /** * Constructor. Called by core. * * @usage configure it in vendor/phppe/Core/config.php * * @param string cache uri */ public function __construct($cfg = null) { if (!empty($cfg)) { self::$uri = $cfg; $m = explode(':', $cfg); $d = '\\PHPPE\\Cache\\'.$m[0]; if (ClassMap::has($d)) { self::$mc = new $d($cfg); } //! if none, fallback to memcache if (empty(self::$mc)) { // @codeCoverageIgnoreStart $d = '\\Memcache'; if (!class_exists($d)) { \PHPPE\Core::log('C', L("no php-memcache"), 'cache'); } //! unix file: "unix:/tmp/fifo", "host" or "host:port" otherwise if ($m[0] == 'unix') { $p = 0; $h = $m[1]; } else { $p = !empty($m[1]) ? $m[1] + 0 : 11211; $h = $m[0]; } // @codeCoverageIgnoreEnd self::$mc = new $d(); //Core::$mc->addServer( $h, $p ); //$s = @Core::$mc->getExtendedStats( ); if (/*empty( $s[$h . ( $p > 0 ? ":" . $p : "" )] ) || */ !@self::$mc->pconnect($h, $p, 1)) { // @codeCoverageIgnoreStart usleep(100); if (!@self::$mc->pconnect($h, $p, 1)) { self::$mc = null; } // @codeCoverageIgnoreEnd } } //! let rest of the world know about us if (is_object(self::$mc)) { $this->name = $d; } else { self::$mc = null; } } //! built-in blobs - referenced as cached objects //! this should go to init(), but we serve them as soon //! as possible to speed up page load // } // @codeCoverageIgnoreStart // function init($cfg) // { if (!empty($_GET['cache'])) { $d = trim($_GET['cache']); switch ($d) { //! inline PHPPE logo case 'logo' : Http::mime('image/png'); $c = 'vendor/phppe/Core/images/.phppe'; die(file_exists($c) ? file_get_contents($c) : base64_decode('R0lGODlhKgAYAMIHAAACAAcAABYAAygBDD4BEFwAGGoBGwWYISH5BAEKAAcALAAAAAAqABgAAAOxeLrcCsDJSSkIoertYOSgBmXh5p3MiT4qJGIw9h3BFZP0LVceU0c91sy1uMwkwQfmYEzhiCwc8sh0QQ+FrMFQIAgY2cIWuUx9LoutWsxNs9udaxDKDb+7Wzth+huRRmlcJANrW148NjJDdF2Db2t7EzUUkwpqAx8EaoWRUyCXgVx5L1QUeQQDBGwFhIYDAxNNHJubBQqPBiWmeWqdWG+6EmrBxJZwxbqjyMnHy87P0BMJADs=')); //! Stylesheet for PHPPE Panel case 'css' : Http::mime('text/css'); $p = 'position:fixed;top:'; $s = 'text-shadow:2px 2px 2px #FFF;'; $c = 'rgba(136,146,191'; die('#pe_p{'.$p."0;z-index:1998;left:0;width:100%;padding:0 2px 0 32px;background-color:$c,0.9);background:linear-gradient($c,0.4),$c,0.6),$c,0.8),$c,0.9),$c,1) 90%,rgba(0,0,0,1));height:31px !important;font-family:helvetica;font-size:14px !important;line-height:20px !important;}#pe_p SPAN{margin:0 5px 0 0;cursor:pointer;}#pe_p UL{list-style-type:none;margin:3px;padding:0;}#pe_p IMG{border:0;vertical-align:middle;padding-right:4px;}#pe_p A{text-decoration:none;color:#000;".$s.'}#pe_p .menu {position:fixed;top:8px;left:90px;}#pe_p .stat SPAN{display:inline-block;'.$s.'}#pe_p LI{cursor:pointer;}#pe_p LI:hover{background:#F0F0F0;}#pe_p .stat{'.$p.'6px;right:48px;}#pe_p .sub{'.$p.'28px;display:inline;background:#FFF;border:solid 1px #808080;box-shadow:2px 2px 6px #000;z-index:1999;}#pe_p .menu_i{padding:5px 6px 5px 6px;'.$s.'}#pe_p .menu_a{padding:4px 5px 5px 5px;border-top:solid #000 1px;border-left:solid #000 1px;border-right:solid #000 1px;background:#FFF;}@media print{#pe_p{display:none;}}'); //! serve real cache requests default : $c = self::get("c_$d"); if (is_array($c) && !empty($c['d'])) { Http::mime((!empty($c['m']) ? $c['m'] : 'text/plain')); die($c['d']); } die('CACHE-E: '.$d); } } // @codeCoverageIgnoreEnd } /** * Set a value in cache. * * @param string key * @param mixed value * @param integer ttl, optional * @param boolean force use of cache, optional */ public static function set($k, $v, $ttl = 0, $force=false) { if (!empty(self::$mc) && (empty(Core::$core->nocache)||$force)) { return @self::$mc->set($k, $v, MEMCACHE_COMPRESSED, $ttl > 0 ? $ttl : Core::$core->cachettl); } return false; } /** * Get a value from cache. * * @param string key * @param boolean force use of cache */ public static function get($k, $force=false) { if (!empty(self::$mc) && (empty(Core::$core->nocache)||$force)) { return self::$mc->get($k); } return; } /** * Clear cache. * */ public static function invalidate() { if (!empty(self::$mc) && method_exists(self::$mc,"invalidate")) { return self::$mc->invalidate(); } else { // fallback $c = !empty(Core::$core->cache) ? Core::$core->cache : self::$uri; if (preg_match("/^([^:]+):?([0-9]*)$/",$c,$m)) { $f = fsockopen($m[1], $m[2]>0?$m[2]:11211, $n, $e, 1); fwrite($f,"flush_all\n"); fclose($f); } } return; } /** * Initialize event. * * @param array configuration array * * @return boolean false if initialization failed */ public function init($cfg) { //! remove Cache from extensions if there's no instance return !empty(self::$mc); } } /** * Assets proxy. It will use memcache if configured * Also takes care of dynamic assets and saves their output. */ class Assets extends Extension { /** * Route event handler. Will look for images, css, js application. * * @param string current application * @param string current action */ // @codeCoverageIgnoreStart public function route($app, $action) { //! proxy dynamic assets (vendor directory is not accessable by the webserver, only public dir) if (in_array($app, ['css', 'js', 'images', 'fonts'])) { //! helper function to specify mime header and minify assets function b($a, $b) { Http::mime($a == 'css' ? 'text/css' : ($a == 'js' ? 'text/javascript' : ($a == 'images'?'image/png':'application/octet-stream'))); die($b); } //! let's try to get it from cache $N = 'a_'.sha1(Core::$core->base.Core::$core->url.'/'.Core::$user->id.'/'.Core::$client->lang); $d = Cache::get($N); if (!empty($d)) { b($app, $d); } else { //! cache miss, we'll have to generate the asset //! remove language code from core.js url. This "alias" allows per language cache foreach ([Core::$core->url, preg_replace("/^js\/core\.[^\.]+\.js/", 'js/core.js', Core::$core->url).'.php',] as $p) { $A = 'vendor/phppe/*/'.strtr($p, ['*' => '', '..' => '']); $c = @glob($A, GLOB_NOSORT)[0]; if (empty($c)) { $A = 'public/'.strtr($p, ['*' => '', '..' => '']); $c = @glob($A, GLOB_NOSORT)[0]; } if ($c) { if (substr($c, -4) != '.php') { //! use file_get_contents and 10 times longer cache ttl for static files $d = file_get_contents($c); Core::$core->cachettl *= 10; } else { //! use include_once for php with normal cache ttl ob_start(); include_once $c; $d = ob_get_clean(); } } if ($d) { $d = self::minify($d, $app); //! save it to the cache for later Cache::set($N, $d); //! output result b($app, $d); } } } //! no asset found by that url header('HTTP/1.1 404 Not Found'); die; } //! not a real asset, but no better place if($app=="passwd"&&Core::$client->ip=="CLI") { echo(chr(27)."[96m".L("Password")."? ".chr(27)."[0m"); system('stty -echo'); $p = rtrim(fgets(STDIN)); system('stty echo'); die("\n".password_hash($p, PASSWORD_BCRYPT, ['cost'=>12])."\n"); } } // @codeCoverageIgnoreEnd /** * Asset minifier. * * @param string data * @param string type, 'css' or 'js' * * @return string minified data */ public static function minify($d, $t = 'js') { //! check input, return output just as is if type unknown if (!empty(Core::$core->nominify) || ($t != 'css' && $t != 'js' && $t != 'php')) { return $d; } $d = trim($d); //! allow use of third party vendor code if ($t == 'css' && class_exists('CSSMin')) return \CSSMin::minify($d); if ($t == 'js' && class_exists('JSMin')) return \JSMin::minify($d); //! do the stuff ourself (fastest, safest, simpliest, and no dependency required at all... and only 70 SLoC) $n = ''; $i = 0; $l = strlen($d); while ($i < $l) { if ($t == 'php' && ($d[$i] == '?' && $d[$i + 1] == '>')) { $j = $i; $i += 2; while ($i < $l && ($d[$i-1] != '<' || $d[$i] != '?')) { $i++; } $i++; $n .= substr($d, $j, $i - $j); continue; } $c = @substr($n, -1); //! string literals if (($d[$i] == "'" || $d[$i] == '"') && $c != '\\') { $s = $d[$i]; $j = $i; ++$i; while ($i < $l && $d[$i] != $s) { if ($d[$i] == '\\') $i++; ++$i; } ++$i; $n .= substr($d, $j, $i - $j); continue; } //! remove comments if ($t != 'css' && ($d[$i] == '/' && $d[$i + 1] == '/')) { $i += 2; while ($i < $l && $d[$i] != "\n") { $i++; } continue; } if ($d[$i] == '/' && $d[$i + 1] == '*') { $i += 2; while ($i + 1 < $l && ($d[$i] != '*' || $d[$i + 1] != '/')) { $i++; } $i += 2; continue; } //! remove tabs and line endings if ($d[$i] == "\t" || $d[$i] == "\r" || $d[$i] == "\n") { //! add a space to separate words if necessary if ( (($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9')) && ($d[$i + 1] == '\\' || $d[$i + 1] == '/' || $d[$i + 1] == '_' || $d[$i + 1] == '*' || ($d[$i + 1] >= 'a' && $d[$i + 1] <= 'z') || ($d[$i + 1] >= 'A' && $d[$i + 1] <= 'Z') || ($d[$i + 1] >= '0' && $d[$i + 1] <= '9') || $d[$i + 1] == '#' || ($t == 'css' && $d[$i+1]=='.')) ) { $n .= ' '; } $i++; continue; } //! remove extra spaces if ($d[$i] == ' ' && $c!='\\' && (!(($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9') || $c=='%' || $c=='-') || !($d[$i + 1] == '\\' || $d[$i + 1] == '/' || $d[$i + 1] == '_' || ($d[$i + 1] >= 'a' && $d[$i + 1] <= 'z') || ($d[$i + 1] >= 'A' && $d[$i + 1] <= 'Z') || ($d[$i + 1] >= '0' && $d[$i + 1] <= '9') || $d[$i + 1] == '#' || $d[$i + 1] == '*' || ($t == 'css' && ($d[$i+1]=='.'||$d[$i+1]=='-'||$c=='-')) || ($t == 'js' && $d[$i + 1] == '$')))) { ++$i; continue; } if($t=="css" && $d[$i]=='(' && (substr($n,-3)=="and"||substr($n,-2)=="or")) $n.=' '; //! copy character to new string $n .= $d[$i++]; } return $n; } } /** * Content Server. This is the default fallback application if * url route failed. */ class Content extends Extension { private static $dds = []; //!< dynamic data sets /** * Constructor. Common code for all Content actions. * * @param string url */ public function __construct($u="") { //! check cache $C = 'd_'.sha1(url().'/'.Core::$user->id.'/'.Core::$client->lang); $data = Cache::get($C); try { //! cache miss, look it up in database - only primary datasource if (empty($data['id'])) { DS::ds(0); if(empty($u)) $u=Core::$core->url; $data = (array)DS::fetch("a.*,b.ctrl", "pages a LEFT JOIN views b ON a.template=b.id", "(a.id=? OR ? LIKE a.id||'/%') AND ". "(a.lang='' OR a.lang=?) AND ".(ClassMap::has("PHPPE\\CMS") && @get_class(View::getval('app'))=="PHPPE\\Content" && Core::$user->has("siteadm|webadm")?"":"a.publishid!=0 AND "). "a.pubd<=CURRENT_TIMESTAMP AND (a.expd='' OR a.expd=0 OR a.expd>CURRENT_TIMESTAMP)", "", "a.id DESC,a.created DESC", [$u, $u, Core::$client->lang] ); if (!empty($data['id'])) { Cache::set($C, $data); } else { return; } } //! check filters if (!empty($data['filter']) && !Core::cf($data['filter'])) { //! not allowed, fallback to 403 Core::$core->template = '403'; return; } //! set view for page Core::$core->template = $data['template']; //! load site title Core::$core->title = $data['name']; //! load application property overrides $o = json_decode($data['data'], true); if (is_array($o)) { foreach ($o as $k => $v) { if(substr($k,0,4)=="app.") $k=substr($k,4); $this->$k = $v; } } foreach (['id', 'name', 'lang', 'modifyd', 'ctrl', 'publishid'] as $k) { $this->$k = $data[$k]; } //! get page specific DDS $E = json_decode($data['dds'], true); if (is_array($E)) { self::$dds += $E; } // @codeCoverageIgnoreStart } catch (\Exception $e) { } // @codeCoverageIgnoreEnd } /** * Default action. * * @param not used. */ public function action($item = '') { //! as this could be considered as a security risk, this feature can be turned off globally if (!empty(Core::$core->noctrl) || empty($this->ctrl)) { return; } ob_start(); //FIXME: sanitize php code eval("namespace PHPPE;\n".$this->ctrl); return ob_get_clean(); } /** * Get dynamic data sets into application properties. * * @param object application instance */ public static function getDDS(&$app) { try { //! special page holds global page parameters and dds' $F = (array)DS::fetch('data,dds', 'pages', "id='frame' AND ".(ClassMap::has("PHPPE\\CMS") && get_class(View::getval('app'))=="PHPPE\\Content" && // bug in php-code-coverage, marks unchecked in middle of an AND expression... // @codeCoverageIgnoreStart Core::$user->has("siteadm|webadm")?"":"publishid!=0 AND "). "(lang='' OR lang=?)", '', 'id DESC,created DESC',[Core::$client->lang]); $E = $F?json_decode($F['data'], true):null; View::assign('frame', $E); //! load global dds $D = $F?json_decode($F['dds'], true):null; if (is_array($D)) { self::$dds += $D; } } catch (\Exception $e) { } // @codeCoverageIgnoreEnd $o = []; foreach (self::$dds as $k => $c) { //! don't allow to set these, as they cannot be arrays if (!in_array($k, ['dds', 'id', 'title', 'mimetype'])) { try { $o[$k] = @DS::query($c[0], @$c[1], strtr(@$c[2], ['@ID' => $k,'@SHA' => sha1($k), '@URL' => Core::$core->url]), @$c[3], @$c[4], @$c[5], View::getval(@$c[6])); foreach ($o[$k] as $i => $v) { $d = @json_decode($v['data'], true); if (is_array($d)) $o[$k][$i] += $d; unset($o[$k][$i]['data']); } } catch (\Exception $e) { Core::log('W', $k.' '.$e->getMessage().' '.implode(' ', $c), 'dds'); } } } //! set application properties if (!empty($o)) { foreach ($o as $k => $v) { $app->$k = $v; } } } } /** * View layer. */ class View extends Extension { private static $hdr = [ 'meta' => [], 'link' => [], 'css' => [], 'js' => [], 'jslib' => [], ]; //!< header items and js libraries private static $menu; //!< system menu, populated by initialized modules private static $n; //!< templater nested level private static $c; //!< templater control structures context private static $o = []; //!< templater objects private static $tc; //!< try button counter private static $p; //!< templater default path for views private static $addons = []; //!< list of initialized widgets public static $e = ''; //!< last expression to evaluate public static $C; //!< php expression cache /** * Initialize event handler. Register basic object in templater * also copy meta and link tags from configuration */ public static function init() { //! register core, user and client to templater self::$o['core'] = Core::$core; //! register default meta keywords if (!empty(Core::$core->meta) && is_array(Core::$core->meta)) { self::$hdr['meta'] = Core::$core->meta; } self::$hdr['meta']['viewport'] = 'width=device-width,initial-scale=1.0'; if (!empty(Core::$core->link) && is_array(Core::$core->link)) { self::$hdr['link'] = Core::$core->link; } //! add core.js with language code in name. This allows separate client side caches //! also give it a high priortity; jQuery has 00, so core.js should have 01 $js = 'vendor/phppe/Core/js/core.js.php'; if (file_exists($js)) { self::$hdr['jslib']['core.'.Core::$client->lang.'.js'] = "01$js"; } //! try button counter self::$tc = 0; } /** * Set default path for templater. Used by Core::run() after appliction class determined * * @param string path */ public static function setPath(&$p) { self::$p = &$p; } /** * Register an object in templater. * * @param string name * @param mixed instance reference */ public static function assign($n, &$o) { self::$o[$n] = &$o; } /** * Register a new stylesheet. * * @param string name of the stylesheet */ public static function css($c = '') { if (empty($c)) { return self::$hdr['css']; } //! add cdn stylesheet if (substr($c, 0, 4) == 'http') { self::$hdr['link'][$c] = 'stylesheet'; } else { //! add a new stylesheet to output $a=@glob("vendor/phppe/*/css/".@explode('?', $c)[0]."*")[0]; if (!isset(self::$hdr['css'][$c]) && !empty($a)) { self::$hdr['css'][$c] = realpath($a); } } } /** * Register a new javascript library. * * @param string name of the js library * @param string if it needs to be initialized, the code to do that * @param integer priority (0=jQuery, 1-9=frameworks, 10-=libraries) */ public static function jslib($l = '', $i = '', $p = 10) { if (empty($l)) { return self::$hdr['jslib']; } if ($p < 0 || $p > 99) $p = 99; //! add cdn javascript if (substr($l, 0, 4) == 'http') { self::$hdr['jslib'][$l] = sprintf('%02d', $p).$l; } else { //! add a new javascript library to output $a=@glob("vendor/phppe/*/js/".@explode('?', $l)[0]."*")[0]; if (!isset(self::$hdr['jslib'][$l]) && !empty($a)) { self::$hdr['jslib'][$l] = sprintf('%02d', $p).realpath($a); } } //! also register init hook and call it on domcomplete event $i = trim($i); if (!empty($i) && (empty(self::$hdr['js']['init()']) || strpos(self::$hdr['js']['init()'], $i) === false)) { self::js('init()', $i.($i[strlen($i) - 1] != ';' ? ';' : ''), true); } } /** * Register a new javascript function. * * @param string name of the js function with arguments * @param string code * @param boolean if code should be appended to existing code, true. Replace otherwise */ public static function js($f = '', $c = '', $a = 0) { if ($c) { //! add a javascript function to output $C = Assets::minify($c, 'js'); $C .= ($C[strlen($C) - 1] != ';' ? ';' : ''); if ($a) { if (strpos(@self::$hdr['js'][$f], $C) === false) { @self::$hdr['js'][$f] .= $C; } } else { self::$hdr['js'][$f] = $C; } } } /** * Register a new menu item or submenu in PHPPE panel. * * @param string title of the link * @param string/array url or array of title=>url */ public static function menu($t = '', $l = '') { if (empty($t)) { return self::$menu; } //! add a new menuitem or submenu if (is_string($l) || is_array($l)) { self::$menu[$t] = $l; } } /** * Load view from cache. Called by Core::run() * * @param string cache key * * @return string cached content or null */ public static function fromCache($N) { $d = Cache::get($N); if (is_array($d)) { //! cache hit, we are happy! foreach (['m' => 'meta', 'c' => 'css', 'j' => 'js', 'J' => 'jslib'] as $k => $v) { if (is_array($d[$k])) { self::$hdr[$v] = array_merge(self::$hdr[$v], $d[$k]); } } return $d['d']; } return ''; } /** * Generate the main part of the view (that is, without html header and footer). * Called by Core::run() once. * * @param string template to use * @param string cache key * * @return string generated content */ public static function generate($template, $N = '') { //! we should check cache here, but it's already handled //! by Core::run() because this code never reached when cached //! clear validators $_SESSION['pe_v'] = []; //! if controller cleared template name, return empty string $T = ""; if (!empty($template)) { $T = self::template($template); //! if action specific template not found, fallback to application's if (empty($T)) $T = self::template(Core::$core->app.'_'.Core::$core->action); if (empty($T)) $T = self::template(Core::$core->app); if (empty($T)) $T = self::template('404'); //! fallback index page if even 404 template missing if (empty($T) && Core::$core->app == 'index') { // @codeCoverageIgnoreStart $T = '