Update DatabaseMysql and VMXTemplate, fix some notices

master
vitalif 2013-04-09 15:14:26 +00:00
parent 5b8968e5e5
commit c10676624d
4 changed files with 264 additions and 87 deletions

View File

@ -3,7 +3,8 @@
/** /**
* Very stable interface for MySQL - object-oriented at last :) * Very stable interface for MySQL - object-oriented at last :)
* Select builder is inspired by MediaWiki's one. * Select builder is inspired by MediaWiki's one.
* (c) Vitaliy Filippov, 2012 * Also usable for querying SphinxQL.
* (c) Vitaliy Filippov, 2012-2013
*/ */
if (!defined('MS_HASH')) if (!defined('MS_HASH'))
@ -31,6 +32,8 @@ class DatabaseMysql implements Database
var $tableNames = array(); var $tableNames = array();
var $queryLogFile; var $queryLogFile;
var $reconnect = true; var $reconnect = true;
var $autoBegin;
var $ondestroy = 'commit';
var $queryCount = 0; var $queryCount = 0;
var $link; var $link;
@ -46,8 +49,11 @@ class DatabaseMysql implements Database
* dbname DB name to use * dbname DB name to use
* username Username * username Username
* password Password * password Password
* reconnect Whether to reconnect on idle timeout [true] * tableNames Table name mappings (virtual => real)
* queryLogFile Path to query log file * queryLogFile Path to query log file
* reconnect Whether to reconnect on idle timeout [true]
* autoBegin Whether to automatically begin a transaction of first query [false]
* ondestroy commit/rollback/none during destruction [commit]
*/ */
function __construct($options) function __construct($options)
{ {
@ -59,7 +65,10 @@ class DatabaseMysql implements Database
'username' => '', 'username' => '',
'password' => '', 'password' => '',
'reconnect' => true, 'reconnect' => true,
'tableNames' => array(),
'queryLogFile' => '', 'queryLogFile' => '',
'autoBegin' => false,
'ondestroy' => 'commit',
); );
$options += $defOpts; $options += $defOpts;
if ($options['socket']) if ($options['socket'])
@ -72,6 +81,16 @@ class DatabaseMysql implements Database
} }
} }
function __destruct()
{
$o = $this->ondestroy;
if (($o === 'commit' || $o === 'rollback') && $this->transactions)
{
$this->transactions = array(false);
$this->$o();
}
}
function connect() function connect()
{ {
if ($this->socket !== NULL) if ($this->socket !== NULL)
@ -96,6 +115,10 @@ class DatabaseMysql implements Database
else else
{ {
$this->link->set_charset('utf8'); $this->link->set_charset('utf8');
if ($this->autoBegin)
{
$this->begin();
}
} }
} }
@ -109,6 +132,18 @@ class DatabaseMysql implements Database
return "`".str_replace("`", "``", $name)."`"; return "`".str_replace("`", "``", $name)."`";
} }
function quoteInto($str, $params)
{
$i = 0;
$r = '';
while (($p = strpos($str, '?')) !== false)
{
$r .= substr($str, 0, $p) . $this->quote($params[$i++]);
$str = substr($str, $p+1);
}
return $r.$str;
}
function quote($value) function quote($value)
{ {
if ($value === NULL) if ($value === NULL)
@ -124,26 +159,88 @@ class DatabaseMysql implements Database
function query($sql, $fetchMode = MYSQLI_STORE_RESULT) function query($sql, $fetchMode = MYSQLI_STORE_RESULT)
{ {
if (!$this->link)
{
$this->connect();
}
$this->queryCount++; $this->queryCount++;
if ($this->queryLogFile) if ($this->queryLogFile)
{ {
file_put_contents($this->queryLogFile, date("[Y-m-d H:i:s] ").$sql."\n", FILE_APPEND); file_put_contents($this->queryLogFile, date("[Y-m-d H:i:s] ").$sql."\n", FILE_APPEND);
} }
$r = $this->link->query($sql, $fetchMode);
if (!$r)
{
if ($this->link->errno == 2006 && $this->reconnect && !$this->transactions)
{
// "MySQL server has gone away"
$this->connect();
$r = $this->link->query($sql, $fetchMode);
if (!$r && $this->link->errno == 2006)
{
$this->link = false;
}
}
if (!$r)
{
throw new DatabaseException($this->link->error . "\nQuery: $sql", $this->link->errno);
}
}
return $r;
}
function multi_select(array $queries, $format = 0)
{
if (!$this->link) if (!$this->link)
{ {
$this->connect(); $this->connect();
} }
$r = $this->link->query($sql, $fetchMode); $this->queryCount += count($queries);
$log = '';
foreach ($queries as &$sql)
{
if (!is_string($sql))
{
$sql = $this->select_builder($sql[0], $sql[1], $sql[2], @$sql[3]);
}
$log .= date("[Y-m-d H:i:s] ").$sql."\n";
}
unset($sql);
if ($this->queryLogFile)
{
file_put_contents($this->queryLogFile, $log, FILE_APPEND);
}
$sql = implode('; ', $queries);
$r = $this->link->multi_query($sql);
if (!$r) if (!$r)
{ {
if ($this->link->errno == 2006 && $this->reconnect) if ($this->link->errno == 2006 && $this->reconnect && !$this->transactions)
{ {
// "MySQL server has gone away" // "MySQL server has gone away"
$this->link = false; $this->connect();
$r = $this->link->multi_query($sql);
if (!$r && $this->link->errno == 2006)
{
$this->link = false;
}
}
if (!$r)
{
throw new DatabaseException($this->link->error, $this->link->errno);
} }
throw new DatabaseException($this->link->error, $this->link->errno);
} }
return $r; $results = array();
$i = 0;
foreach ($queries as $k => $q)
{
if ($i++)
{
$this->link->next_result();
}
$r = $this->link->store_result();
$results[$k] = $this->fetch_all($r, $format);
}
return $results;
} }
/** /**
@ -220,6 +317,8 @@ class DatabaseMysql implements Database
* 'conditional expression', * 'conditional expression',
* 'field_name' => 'value', * 'field_name' => 'value',
* 'field_name' => array('one', 'of', 'values'), * 'field_name' => array('one', 'of', 'values'),
* 'field_name < ?' => 'value',
* 'field_name < DATE_SUB(?, ?)' => array('arg1', 'arg2'),
* 'field1,field2' => array(array(1, 2), array(3, 4)), * 'field1,field2' => array(array(1, 2), array(3, 4)),
* ) * )
*/ */
@ -231,7 +330,21 @@ class DatabaseMysql implements Database
foreach ($where as $k => $v) foreach ($where as $k => $v)
{ {
if (ctype_digit("$k")) if (ctype_digit("$k"))
$wh[] = $v; {
if (is_array($v))
{
$str = array_shift($v);
$wh[] = $this->quoteInto($str, $v);
}
else
{
$wh[] = $v;
}
}
elseif (($p = strrpos($k, '?')) !== false)
{
$wh[] = $this->quoteInto($k, (array)$v);
}
elseif (is_array($v)) elseif (is_array($v))
{ {
if (!$v) if (!$v)
@ -240,7 +353,7 @@ class DatabaseMysql implements Database
} }
else else
{ {
if (is_array($v[0])) if (is_array(reset($v)))
foreach ($v as &$l) foreach ($v as &$l)
$l = "(" . implode(",", array_map(array($this, 'quote'), $l)) . ")"; $l = "(" . implode(",", array_map(array($this, 'quote'), $l)) . ")";
else else
@ -248,15 +361,22 @@ class DatabaseMysql implements Database
$wh[] = "$k IN (" . implode(",", $v) . ")"; $wh[] = "$k IN (" . implode(",", $v) . ")";
} }
} }
elseif (preg_match('/^-?\d+(\.\d+)?$/s', $v)) // int/float
$wh[] = "$k=$v";
elseif ($v !== NULL) elseif ($v !== NULL)
$wh[] = "$k=".$this->quote($v); $wh[] = "$k=".$this->quote($v);
else else
$wh[] = "$k IS NULL"; $wh[] = "$k IS NULL";
} }
if (!$wh) if (!$this->username && !$this->password && !$this->dbname)
$where = '1'; {
else // Sphinx supports neither brackets nor OR operator as of 2.0.6-release O_o
$where = join(' AND ', $wh);
}
elseif ($where)
$where = '(' . join(') AND (', $wh) . ')'; $where = '(' . join(') AND (', $wh) . ')';
else
$where = '';
return $where; return $where;
} }
@ -275,8 +395,11 @@ class DatabaseMysql implements Database
* 'FOR UPDATE' or 'LOCK IN SHARE MODE' * 'FOR UPDATE' or 'LOCK IN SHARE MODE'
* 'GROUP BY' => array($groupby_field1 => 'ASC', $groupby_field2 => 'DESC') * 'GROUP BY' => array($groupby_field1 => 'ASC', $groupby_field2 => 'DESC')
* 'ORDER BY' => array($orderby_field1 => 'ASC', $orderby_field2 => 'DESC') * 'ORDER BY' => array($orderby_field1 => 'ASC', $orderby_field2 => 'DESC')
* 'LIMIT' => array($limit, $offset) or array($limit) or just $limit * 'LIMIT' => array($offset, $limit) or array($limit) or just $limit
* 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit * 'OFFSET' => $offset, for the case when 'LIMIT' is just $limit
*
* Sphinx Search extensions:
* 'WITHIN GROUP ORDER BY' => array($orderby_field => 'ASC')
*/ */
function select_builder($tables, $fields, $where, $options = NULL) function select_builder($tables, $fields, $where, $options = NULL)
{ {
@ -304,40 +427,23 @@ class DatabaseMysql implements Database
$sql .= 'SQL_NO_CACHE '; $sql .= 'SQL_NO_CACHE ';
elseif (isset($options['CACHE']) || isset($options['SQL_CACHE'])) elseif (isset($options['CACHE']) || isset($options['SQL_CACHE']))
$sql .= 'SQL_CACHE '; $sql .= 'SQL_CACHE ';
$sql .= "$fields FROM $tables WHERE $where"; $sql .= "$fields FROM $tables";
if (isset($options['GROUP BY'])) if ($where)
{ {
$g = $options['GROUP BY']; $sql .= " WHERE $where";
if (is_array($g))
{
$g1 = array();
foreach ($g as $k => $v)
{
if (ctype_digit("$k"))
$g1[] = $v;
else
$g1[] = "$k $v";
}
$g = join(',', $g1);
}
$sql .= " GROUP BY $g";
} }
if (isset($options['ORDER BY'])) if (!empty($options['GROUP BY']) && $options['GROUP BY'] !== '0')
{ {
$g = $options['ORDER BY']; $sql .= " GROUP BY ".$this->order_option($options['GROUP BY']);
if (is_array($g)) }
{ if (!empty($options['ORDER BY']) && $options['GROUP BY'] !== '0')
$g1 = array(); {
foreach ($g as $k => $v) $sql .= " ORDER BY ".$this->order_option($options['ORDER BY']);
{ }
if (ctype_digit("$k")) if (!empty($options['WITHIN GROUP ORDER BY']) && $options['WITHIN GROUP GROUP BY'] !== '0')
$g1[] = $v; {
else // Sphinx Search extension
$g1[] = "$k $v"; $sql .= " WITHIN GROUP ORDER BY ".$this->order_option($options['WITHIN GROUP ORDER BY']);
}
$g = join(',', $g1);
}
$sql .= " ORDER BY $g";
} }
$sql .= $this->limit_option($options); $sql .= $this->limit_option($options);
if (isset($options['FOR UPDATE'])) if (isset($options['FOR UPDATE']))
@ -347,10 +453,30 @@ class DatabaseMysql implements Database
return $sql; return $sql;
} }
/**
* Handles ORDER BY or GROUP BY options
*/
protected function order_option($g)
{
if (is_array($g))
{
$g1 = array();
foreach ($g as $k => $v)
{
if (ctype_digit("$k"))
$g1[] = $v;
else
$g1[] = "$k $v";
}
$g = join(',', $g1);
}
return $g;
}
/** /**
* Handles a single LIMIT or LIMIT and OFFSET options. * Handles a single LIMIT or LIMIT and OFFSET options.
*/ */
function limit_option($options) protected function limit_option($options)
{ {
if (isset($options['LIMIT'])) if (isset($options['LIMIT']))
{ {
@ -400,7 +526,7 @@ class DatabaseMysql implements Database
else else
$table = (isset($this->tableNames[$v[1]]) ? $this->quoteId($this->tableNames[$v[1]]) : $v[1]) . ' ' . $k; $table = (isset($this->tableNames[$v[1]]) ? $this->quoteId($this->tableNames[$v[1]]) : $v[1]) . ' ' . $k;
if ($t) if ($t)
$t .= " $join JOIN $table ON ".$this->where_builder($v[2]); $t .= " $join JOIN $table ON ".($this->where_builder($v[2]) ?: '1=1');
else else
$t = $table; $t = $table;
continue; continue;
@ -448,7 +574,7 @@ class DatabaseMysql implements Database
*/ */
function select($tables, $fields = '*', $where = 1, $options = NULL, $format = 0) function select($tables, $fields = '*', $where = 1, $options = NULL, $format = 0)
{ {
if (is_int($fields) || is_string($fields) && ctype_digit($fields)) if (is_int($fields))
{ {
$sql = $tables; $sql = $tables;
$format = $fields; $format = $fields;
@ -457,10 +583,16 @@ class DatabaseMysql implements Database
$sql = $this->select_builder($tables, $fields, $where, $options); $sql = $this->select_builder($tables, $fields, $where, $options);
if ($format & MS_RESULT) if ($format & MS_RESULT)
return $this->query($sql, MYSQLI_USE_RESULT); return $this->query($sql, MYSQLI_USE_RESULT);
$res = $this->query($sql);
return $this->fetch_all($res, $format);
}
function fetch_all($res, $format = 0)
{
if ($format & MS_LIST) if ($format & MS_LIST)
$r = $this->get_rows($sql); $r = $this->get_rows($res);
else else
$r = $this->get_assocs($sql); $r = $this->get_assocs($res);
if (!$r) if (!$r)
$r = array(); $r = array();
if ($format & MS_ROW) if ($format & MS_ROW)
@ -478,6 +610,7 @@ class DatabaseMysql implements Database
} }
elseif ($format & MS_COL) elseif ($format & MS_COL)
{ {
$k = false;
foreach ($r as $i => $v) foreach ($r as $i => $v)
{ {
if (!$k) if (!$k)
@ -507,7 +640,7 @@ class DatabaseMysql implements Database
function delete($tables, $where, $options = NULL) function delete($tables, $where, $options = NULL)
{ {
$tables = $this->tables_builder($tables); $tables = $this->tables_builder($tables);
$where = $this->where_builder($where); $where = $this->where_builder($where) ?: '1=1';
$sql = "DELETE FROM $tables WHERE $where"; $sql = "DELETE FROM $tables WHERE $where";
$sql .= $this->limit_option($options); $sql .= $this->limit_option($options);
$this->query($sql); $this->query($sql);
@ -518,8 +651,9 @@ class DatabaseMysql implements Database
* Builds an INSERT query. * Builds an INSERT query.
* @param string $table Table name to insert rows to. * @param string $table Table name to insert rows to.
* @param array $rows Array of table rows to be inserted. * @param array $rows Array of table rows to be inserted.
* @param boolean $onduplicatekey If true, include * @param boolean $onduplicatekey If true, create MySQL-specific "UPSERT" query using
* ON DUPLICATE KEY UPDATE column=VALUES(column) for all columns. * INSERT .. ON DUPLICATE KEY UPDATE column=VALUES(column) for all columns.
* @param boolean $replace If true, use REPLACE instead of INSERT
*/ */
function insert_builder($table, $rows, $onduplicatekey = false, $replace = false) function insert_builder($table, $rows, $onduplicatekey = false, $replace = false)
{ {
@ -535,8 +669,9 @@ class DatabaseMysql implements Database
$rs[] = $this->quote($r[$k]); $rs[] = $this->quote($r[$k]);
$r = "(".implode(",", $rs).")"; $r = "(".implode(",", $rs).")";
} }
$sphinx = !$this->username && !$this->password && !$this->dbname;
foreach ($key as &$k) foreach ($key as &$k)
if (strpos($k, '`') === false) if (strpos($k, '`') === false && (!$sphinx || $k !== 'id'))
$k = $this->quoteId($k); $k = $this->quoteId($k);
$sql = ($replace ? "REPLACE" : "INSERT"). $sql = ($replace ? "REPLACE" : "INSERT").
" INTO $table (".implode(",",$key).") VALUES ".implode(",",$rows); " INTO $table (".implode(",",$key).") VALUES ".implode(",",$rows);
@ -592,21 +727,19 @@ class DatabaseMysql implements Database
return false; return false;
if (!is_array(@$rows[0])) if (!is_array(@$rows[0]))
$rows = array($rows); $rows = array($rows);
$sql = $this->insert_builder($table, $rows, true, !empty($options['replace'])); $sql = $this->insert_builder($table, $rows, empty($options['REPLACE']), !empty($options['REPLACE']));
} }
else else
{ {
$sql = array(); $sql = array();
if (!is_array($rows)) foreach ((array)$rows as $k => $v)
$rows = array($rows);
foreach ($rows as $k => $v)
{ {
if (!ctype_digit("$k")) if (!ctype_digit("$k"))
$sql[] = $k.'='.$this->quote($v); $sql[] = $k.'='.$this->quote($v);
else else
$sql[] = $v; $sql[] = $v;
} }
$where = $this->where_builder($where); $where = $this->where_builder($where) ?: '1=1';
$sql = 'UPDATE ' . $this->tables_builder($table) . ' SET ' . implode(', ', $sql) . ' WHERE ' . $where; $sql = 'UPDATE ' . $this->tables_builder($table) . ' SET ' . implode(', ', $sql) . ' WHERE ' . $where;
$sql .= $this->limit_option($options); $sql .= $this->limit_option($options);
} }
@ -615,10 +748,16 @@ class DatabaseMysql implements Database
return false; return false;
} }
function get_rows($sql) function replace($table, $rows, $where = NULL, $options = array())
{
$options['REPLACE'] = true;
return $this->update($table, $rows, $where, $options);
}
protected function get_rows($res)
{ {
$r = array(); $r = array();
if ($res = $this->query($sql)) if ($res)
{ {
while ($row = $res->fetch_row()) while ($row = $res->fetch_row())
$r[] = $row; $r[] = $row;
@ -627,10 +766,10 @@ class DatabaseMysql implements Database
return $r; return $r;
} }
function get_assocs($sql) protected function get_assocs($res)
{ {
$r = array(); $r = array();
if ($res = $this->query($sql)) if ($res)
{ {
while ($row = $res->fetch_assoc()) while ($row = $res->fetch_assoc())
$r[] = $row; $r[] = $row;

View File

@ -8,7 +8,9 @@
# Homepage: http://yourcmc.ru/wiki/VMX::Template # Homepage: http://yourcmc.ru/wiki/VMX::Template
# Author: Vitaliy Filippov, 2006-2013 # Author: Vitaliy Filippov, 2006-2013
# $Id$ # $Id: template.php 1394 2013-04-09 15:01:39Z vitalif $
# TODO for perl version - rewrite it and prevent auto-vivification on a.b
class VMXTemplateState class VMXTemplateState
{ {
@ -48,7 +50,7 @@ class VMXTemplate
static $Mon, $mon, $Wday; static $Mon, $mon, $Wday;
static $cache_type = NULL; static $cache_type = NULL;
static $cache = array(); static $cache = array();
static $safe_tags = '<div> <span> <a> <b> <i> <u> <p> <h1> <h2> <h3> <h4> <h5> <h6> <strike> <strong> <small> <big> <blink> <center> <ol> <pre> <sub> <sup> <font> <br> <table> <tr> <td> <th> <tbody> <tfoot> <thead> <tt> <ul> <li> <em> <img> <marquee>'; static $safe_tags = '<div> <blockquote> <span> <a> <b> <i> <u> <p> <h1> <h2> <h3> <h4> <h5> <h6> <strike> <strong> <small> <big> <blink> <center> <ol> <pre> <sub> <sup> <font> <br> <table> <tr> <td> <th> <tbody> <tfoot> <thead> <tt> <ul> <li> <em> <img> <marquee>';
// Timestamp format constants // Timestamp format constants
const TS_UNIX = 0; const TS_UNIX = 0;
@ -281,8 +283,7 @@ class VMXTemplate
} }
if (!class_exists($class) || !isset($class::$version) || $class::$version < self::CODE_VERSION) if (!class_exists($class) || !isset($class::$version) || $class::$version < self::CODE_VERSION)
{ {
// Cache file from some older version - reset it $this->options->error("MD5 collision :) file=$fn, cache=$file", true);
$this->options->error("Please, clear template cache path after upgrading VMX::Template", true);
$this->failed[$fn] = true; $this->failed[$fn] = true;
return NULL; return NULL;
} }
@ -440,8 +441,8 @@ class VMXTemplate
*/ */
static function filter_strip_space(&$text) static function filter_strip_space(&$text)
{ {
$text = preg_replace('/^[ \t\v]+/m', '', $text); $text = preg_replace('/^[ \t]+/m', '', $text);
$text = preg_replace('/[ \t\v]+$/m', '', $text); $text = preg_replace('/[ \t]+$/m', '', $text);
} }
/*** Function implementations ***/ /*** Function implementations ***/
@ -615,6 +616,22 @@ class VMXTemplate
return mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 0, 1); return mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 0, 1);
} }
// Select one of 3 plural forms for russian language
static function plural_ru($count, $one, $few, $many)
{
$sto = $count % 100;
if ($sto >= 10 && $sto <= 20)
return $many;
switch ($count % 10)
{
case 1: return $one;
case 2:
case 3:
case 4: return $few;
}
return $many;
}
// Limited-edition timestamp parser // Limited-edition timestamp parser
static function timestamp($ts = 0, $format = 0) static function timestamp($ts = 0, $format = 0)
{ {
@ -628,7 +645,7 @@ class VMXTemplate
{ {
// TS_UNIX or Epoch // TS_UNIX or Epoch
if (!$ts) if (!$ts)
$ts = time; $ts = time();
} }
elseif (preg_match('/^\D*(\d{4,})\D*(\d{2})\D*(\d{2})\D*(?:(\d{2})\D*(\d{2})\D*(\d{2})\D*([\+\- ]\d{2}\D*)?)?$/s', $ts, $m)) elseif (preg_match('/^\D*(\d{4,})\D*(\d{2})\D*(\d{2})\D*(?:(\d{2})\D*(\d{2})\D*(\d{2})\D*([\+\- ]\d{2}\D*)?)?$/s', $ts, $m))
{ {
@ -804,16 +821,17 @@ class VMXTemplateParser
var $tokens, $tokpos, $tokline, $ptr; var $tokens, $tokpos, $tokline, $ptr;
// Possible tokens consisting of special characters // Possible tokens consisting of special characters
static $chartokens = '+ - = * / % ! , . < > ( ) { } [ ] | .. || && == != <= >= =>'; static $chartokens = '# + - = * / % ! , . < > ( ) { } [ ] & .. || && == != <= >= =>';
// ops_and: ops_eq | ops_eq "&&" ops_and | ops_eq "AND" ops_and // ops_and: ops_bitand | ops_bitand "&&" ops_and | ops_bitand "AND" ops_and
// ops_bitand: ops_eq | ops_eq "&" ops_bitand
// ops_eq: ops_cmp | ops_cmp "==" ops_cmp | ops_cmp "!=" ops_cmp // ops_eq: ops_cmp | ops_cmp "==" ops_cmp | ops_cmp "!=" ops_cmp
// ops_cmp: ops_add | ops_add '<' ops_add | ops_add '>' ops_add | ops_add "<=" ops_add | ops_add ">=" ops_add // ops_cmp: ops_add | ops_add '<' ops_add | ops_add '>' ops_add | ops_add "<=" ops_add | ops_add ">=" ops_add
// ops_add: ops_mul | ops_mul '+' ops_add | ops_mul '-' ops_add // ops_add: ops_mul | ops_mul '+' ops_add | ops_mul '-' ops_add
// ops_mul: exp_neg | exp_neg '*' ops_mul | exp_neg '/' ops_mul | exp_neg '%' ops_mul // ops_mul: exp_neg | exp_neg '*' ops_mul | exp_neg '/' ops_mul | exp_neg '%' ops_mul
static $ops = array( static $ops = array(
'or' => array(array('||', '$or', '$xor'), 'and', true), 'and' => array(array('&&', '$and'), 'bitand', true),
'and' => array(array('&&', '$and'), 'eq', true), 'bitand' => array(array('&'), 'eq', true),
'eq' => array(array('==', '!='), 'cmp', false), 'eq' => array(array('==', '!='), 'cmp', false),
'cmp' => array(array('<', '>', '<=', '>='), 'add', false), 'cmp' => array(array('<', '>', '<=', '>='), 'add', false),
'add' => array(array('+', '-'), 'mul', true), 'add' => array(array('+', '-'), 'mul', true),
@ -1093,8 +1111,8 @@ class VMXTemplateParser
} }
$text = "Unexpected $tok, expected "; $text = "Unexpected $tok, expected ";
if (count($expected) > 1) if (count($expected) > 1)
$text .= "one of '"; $text .= "one of ";
$text .= implode("', '", $expected)."'"; $text .= "'".implode("', '", $expected)."'";
$this->raise($text); $this->raise($text);
} }
@ -1200,10 +1218,23 @@ class VMXTemplateParser
try try
{ {
// Try to parse from here, skip invalid parts // Try to parse from here, skip invalid parts
$r = $this->$handler(); if ($this->tok() == '#')
$this->consume($this->eod); {
// Add newline count from code fragment // Comment!
$this->lineno += substr_count($this->code, "\n", $pos, $this->pos-$pos); $this->pos = strpos($this->code, $this->eod, $this->pos);
if ($this->pos === false)
{
throw new VMXTemplateParseException($this->eod . ' not found');
}
$this->pos += strlen($this->eod);
}
else
{
$r = $this->$handler();
$this->consume($this->eod);
// Add newline count from code fragment
$this->lineno += substr_count($this->code, "\n", $pos, $this->pos-$pos);
}
} }
catch (VMXTemplateParseException $e) catch (VMXTemplateParseException $e)
{ {
@ -1502,6 +1533,7 @@ $varref = array_pop(\$stack);";
if ($this->tok() == ',') if ($this->tok() == ',')
$this->ptr++; $this->ptr++;
} }
$this->ptr++;
} }
$code = false; $code = false;
if ($this->tok() == '=') if ($this->tok() == '=')
@ -1842,7 +1874,7 @@ $varref_index = \$stack[count(\$stack)-1]++;";
} }
$r = "\$this->parent->call_block($parts[0], $args, \"".addslashes($this->errorinfo())."\")"; $r = "\$this->parent->call_block($parts[0], $args, \"".addslashes($this->errorinfo())."\")";
} }
if (count($parts) == 1) elseif (count($parts) == 1)
{ {
$fn = strtolower($parts[0]); $fn = strtolower($parts[0]);
if (isset(self::$functions[$fn])) if (isset(self::$functions[$fn]))
@ -2005,6 +2037,7 @@ $varref_index = \$stack[count(\$stack)-1]++;";
function function_strip($e, $t='') { return "strip_tags($e".($t?",$t":"").")"; } function function_strip($e, $t='') { return "strip_tags($e".($t?",$t":"").")"; }
/* удаление "небезопасных" HTML-тегов */ /* удаление "небезопасных" HTML-тегов */
/* TODO: м.б исправлять некорректную разметку? */
function function_strip_unsafe($e) { return "strip_tags($e, self::\$safe_tags)"; } function function_strip_unsafe($e) { return "strip_tags($e, self::\$safe_tags)"; }
/* заменить \n на <br /> */ /* заменить \n на <br /> */
@ -2036,10 +2069,9 @@ $varref_index = \$stack[count(\$stack)-1]++;";
} }
/* strftime */ /* strftime */
function function_strftime($fmt, $date, $time = '') function function_strftime($fmt, $date)
{ {
$e = $time ? "($date).' '.($time)" : $date; return "strftime($fmt, self::timestamp($date))";
return "strftime($fmt, self::timestamp($e))";
} }
/* ограничение длины строки $maxlen символами на границе пробелов и добавление '...', если что. */ /* ограничение длины строки $maxlen символами на границе пробелов и добавление '...', если что. */
@ -2050,6 +2082,12 @@ $varref_index = \$stack[count(\$stack)-1]++;";
return "self::" . ($this->options->use_utf8 ? "mb_" : "") . "strlimit(".join(",", $a).")"; return "self::" . ($this->options->use_utf8 ? "mb_" : "") . "strlimit(".join(",", $a).")";
} }
/* выбор правильной формы множественного числа для русского языка */
function function_plural_ru($count, $one, $few, $many)
{
return "self::plural_ru($count, $one, $few, $many)";
}
/** Массивы и хеши **/ /** Массивы и хеши **/
/* создание хеша */ /* создание хеша */

View File

@ -343,7 +343,7 @@ class OLAP
} }
$vars = array('build' => 1); $vars = array('build' => 1);
if ($this->group_fields[0]['type'] == 'graph') if ($this->group_fields && $this->group_fields[0]['type'] == 'graph')
{ {
$vars['graphs'] = $this->build_graphs($this->group_fields, $data, $tdkeys); $vars['graphs'] = $this->build_graphs($this->group_fields, $data, $tdkeys);
$vars['groups'] = $this->tpl_jsgraphs($vars['graphs']); $vars['groups'] = $this->tpl_jsgraphs($vars['graphs']);
@ -682,7 +682,7 @@ class OLAP
$i = count($tdhead); $i = count($tdhead);
foreach ($cells as &$v) foreach ($cells as &$v)
{ {
if (!$rows[$i]) if (empty($rows[$i]))
$rows[$i] = array(); $rows[$i] = array();
$rows[$i] = array_merge($rows[$i], $v); $rows[$i] = array_merge($rows[$i], $v);
$i++; $i++;

View File

@ -236,7 +236,7 @@ function doPlot(logarithmic)
<!-- SET o = 0 --> <!-- SET o = 0 -->
<!-- FOR d = graph.desc --> <!-- FOR d = graph.desc -->
<!--# Описание графика (поле: значение, ..., только значения, не равные значениям предыдущего графика) --> <!--# Описание графика (поле: значение, ..., только значения, не равные значениям предыдущего графика) -->
<!-- SET o = or(o, not(graph_index), ne(get(get(get(get(graphs,sub(graph_index,1)),'desc'),d_index),'value'),d.value)) --> <!-- SET o = o || !graph_index || graphs[graph_index-1].desc[d_index].value != d.value -->
<!-- IF o --> <!-- IF o -->
<p style="margin-left: {mul(d_index,20)}px">{d.field}<!-- IF d.func --> ({lc d.func})<!-- END -->: {d.value}</p> <p style="margin-left: {mul(d_index,20)}px">{d.field}<!-- IF d.func --> ({lc d.func})<!-- END -->: {d.value}</p>
<!-- END --> <!-- END -->
@ -271,7 +271,7 @@ function doPlot(logarithmic)
<!-- IF table.desc --> <!-- IF table.desc -->
<!-- SET o = 0 --> <!-- SET o = 0 -->
<!-- FOR d = table.desc --> <!-- FOR d = table.desc -->
<!-- SET o = or(o, not(table_index), ne(get(get(get(get(tables,sub(table_index,1)),'desc'),d_index),'value'),d.value)) --> <!-- SET o = o || !table_index || tables[table_index-1].desc[d_index].value != d.value -->
<!-- IF o --> <!-- IF o -->
<p style="margin-left: {mul(d_index,20)}px">{d.field}<!-- IF d.func --> ({lc d.func})<!-- END -->: {d.value}</p> <p style="margin-left: {mul(d_index,20)}px">{d.field}<!-- IF d.func --> ({lc d.func})<!-- END -->: {d.value}</p>
<!-- END --> <!-- END -->