diff --git a/lib/DatabaseMysql.php b/lib/DatabaseMysql.php index 3c83726..869068a 100644 --- a/lib/DatabaseMysql.php +++ b/lib/DatabaseMysql.php @@ -3,7 +3,8 @@ /** * Very stable interface for MySQL - object-oriented at last :) * 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')) @@ -31,6 +32,8 @@ class DatabaseMysql implements Database var $tableNames = array(); var $queryLogFile; var $reconnect = true; + var $autoBegin; + var $ondestroy = 'commit'; var $queryCount = 0; var $link; @@ -46,8 +49,11 @@ class DatabaseMysql implements Database * dbname DB name to use * username Username * password Password - * reconnect Whether to reconnect on idle timeout [true] + * tableNames Table name mappings (virtual => real) * 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) { @@ -59,7 +65,10 @@ class DatabaseMysql implements Database 'username' => '', 'password' => '', 'reconnect' => true, + 'tableNames' => array(), 'queryLogFile' => '', + 'autoBegin' => false, + 'ondestroy' => 'commit', ); $options += $defOpts; 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() { if ($this->socket !== NULL) @@ -96,6 +115,10 @@ class DatabaseMysql implements Database else { $this->link->set_charset('utf8'); + if ($this->autoBegin) + { + $this->begin(); + } } } @@ -109,6 +132,18 @@ class DatabaseMysql implements Database 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) { if ($value === NULL) @@ -124,26 +159,88 @@ class DatabaseMysql implements Database function query($sql, $fetchMode = MYSQLI_STORE_RESULT) { + if (!$this->link) + { + $this->connect(); + } $this->queryCount++; if ($this->queryLogFile) { 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) { $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 ($this->link->errno == 2006 && $this->reconnect) + if ($this->link->errno == 2006 && $this->reconnect && !$this->transactions) { // "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', * 'field_name' => 'value', * '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)), * ) */ @@ -231,7 +330,21 @@ class DatabaseMysql implements Database foreach ($where as $k => $v) { 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)) { if (!$v) @@ -240,7 +353,7 @@ class DatabaseMysql implements Database } else { - if (is_array($v[0])) + if (is_array(reset($v))) foreach ($v as &$l) $l = "(" . implode(",", array_map(array($this, 'quote'), $l)) . ")"; else @@ -248,15 +361,22 @@ class DatabaseMysql implements Database $wh[] = "$k IN (" . implode(",", $v) . ")"; } } + elseif (preg_match('/^-?\d+(\.\d+)?$/s', $v)) // int/float + $wh[] = "$k=$v"; elseif ($v !== NULL) $wh[] = "$k=".$this->quote($v); else $wh[] = "$k IS NULL"; } - if (!$wh) - $where = '1'; - else + if (!$this->username && !$this->password && !$this->dbname) + { + // 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) . ')'; + else + $where = ''; return $where; } @@ -275,8 +395,11 @@ class DatabaseMysql implements Database * 'FOR UPDATE' or 'LOCK IN SHARE MODE' * 'GROUP BY' => array($groupby_field1 => 'ASC', $groupby_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 + * + * Sphinx Search extensions: + * 'WITHIN GROUP ORDER BY' => array($orderby_field => 'ASC') */ function select_builder($tables, $fields, $where, $options = NULL) { @@ -304,40 +427,23 @@ class DatabaseMysql implements Database $sql .= 'SQL_NO_CACHE '; elseif (isset($options['CACHE']) || isset($options['SQL_CACHE'])) $sql .= 'SQL_CACHE '; - $sql .= "$fields FROM $tables WHERE $where"; - if (isset($options['GROUP BY'])) + $sql .= "$fields FROM $tables"; + if ($where) { - $g = $options['GROUP BY']; - 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"; + $sql .= " WHERE $where"; } - if (isset($options['ORDER BY'])) + if (!empty($options['GROUP BY']) && $options['GROUP BY'] !== '0') { - $g = $options['ORDER BY']; - 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 .= " ORDER BY $g"; + $sql .= " GROUP BY ".$this->order_option($options['GROUP BY']); + } + if (!empty($options['ORDER BY']) && $options['GROUP BY'] !== '0') + { + $sql .= " ORDER BY ".$this->order_option($options['ORDER BY']); + } + if (!empty($options['WITHIN GROUP ORDER BY']) && $options['WITHIN GROUP GROUP BY'] !== '0') + { + // Sphinx Search extension + $sql .= " WITHIN GROUP ORDER BY ".$this->order_option($options['WITHIN GROUP ORDER BY']); } $sql .= $this->limit_option($options); if (isset($options['FOR UPDATE'])) @@ -347,10 +453,30 @@ class DatabaseMysql implements Database 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. */ - function limit_option($options) + protected function limit_option($options) { if (isset($options['LIMIT'])) { @@ -400,7 +526,7 @@ class DatabaseMysql implements Database else $table = (isset($this->tableNames[$v[1]]) ? $this->quoteId($this->tableNames[$v[1]]) : $v[1]) . ' ' . $k; 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 $t = $table; continue; @@ -448,7 +574,7 @@ class DatabaseMysql implements Database */ 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; $format = $fields; @@ -457,10 +583,16 @@ class DatabaseMysql implements Database $sql = $this->select_builder($tables, $fields, $where, $options); if ($format & MS_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) - $r = $this->get_rows($sql); + $r = $this->get_rows($res); else - $r = $this->get_assocs($sql); + $r = $this->get_assocs($res); if (!$r) $r = array(); if ($format & MS_ROW) @@ -478,6 +610,7 @@ class DatabaseMysql implements Database } elseif ($format & MS_COL) { + $k = false; foreach ($r as $i => $v) { if (!$k) @@ -507,7 +640,7 @@ class DatabaseMysql implements Database function delete($tables, $where, $options = NULL) { $tables = $this->tables_builder($tables); - $where = $this->where_builder($where); + $where = $this->where_builder($where) ?: '1=1'; $sql = "DELETE FROM $tables WHERE $where"; $sql .= $this->limit_option($options); $this->query($sql); @@ -518,8 +651,9 @@ class DatabaseMysql implements Database * Builds an INSERT query. * @param string $table Table name to insert rows to. * @param array $rows Array of table rows to be inserted. - * @param boolean $onduplicatekey If true, include - * ON DUPLICATE KEY UPDATE column=VALUES(column) for all columns. + * @param boolean $onduplicatekey If true, create MySQL-specific "UPSERT" query using + * 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) { @@ -535,8 +669,9 @@ class DatabaseMysql implements Database $rs[] = $this->quote($r[$k]); $r = "(".implode(",", $rs).")"; } + $sphinx = !$this->username && !$this->password && !$this->dbname; foreach ($key as &$k) - if (strpos($k, '`') === false) + if (strpos($k, '`') === false && (!$sphinx || $k !== 'id')) $k = $this->quoteId($k); $sql = ($replace ? "REPLACE" : "INSERT"). " INTO $table (".implode(",",$key).") VALUES ".implode(",",$rows); @@ -592,21 +727,19 @@ class DatabaseMysql implements Database return false; if (!is_array(@$rows[0])) $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 { $sql = array(); - if (!is_array($rows)) - $rows = array($rows); - foreach ($rows as $k => $v) + foreach ((array)$rows as $k => $v) { if (!ctype_digit("$k")) $sql[] = $k.'='.$this->quote($v); else $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 .= $this->limit_option($options); } @@ -615,10 +748,16 @@ class DatabaseMysql implements Database 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(); - if ($res = $this->query($sql)) + if ($res) { while ($row = $res->fetch_row()) $r[] = $row; @@ -627,10 +766,10 @@ class DatabaseMysql implements Database return $r; } - function get_assocs($sql) + protected function get_assocs($res) { $r = array(); - if ($res = $this->query($sql)) + if ($res) { while ($row = $res->fetch_assoc()) $r[] = $row; diff --git a/lib/template.php b/lib/template.php index 6278946..c2adfe5 100644 --- a/lib/template.php +++ b/lib/template.php @@ -8,7 +8,9 @@ # Homepage: http://yourcmc.ru/wiki/VMX::Template # 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 { @@ -48,7 +50,7 @@ class VMXTemplate static $Mon, $mon, $Wday; static $cache_type = NULL; static $cache = array(); - static $safe_tags = '

        
    • '; + static $safe_tags = '
    • '; // Timestamp format constants const TS_UNIX = 0; @@ -281,8 +283,7 @@ class VMXTemplate } if (!class_exists($class) || !isset($class::$version) || $class::$version < self::CODE_VERSION) { - // Cache file from some older version - reset it - $this->options->error("Please, clear template cache path after upgrading VMX::Template", true); + $this->options->error("MD5 collision :) file=$fn, cache=$file", true); $this->failed[$fn] = true; return NULL; } @@ -440,8 +441,8 @@ class VMXTemplate */ static function filter_strip_space(&$text) { - $text = preg_replace('/^[ \t\v]+/m', '', $text); - $text = preg_replace('/[ \t\v]+$/m', '', $text); + $text = preg_replace('/^[ \t]+/m', '', $text); + $text = preg_replace('/[ \t]+$/m', '', $text); } /*** Function implementations ***/ @@ -615,6 +616,22 @@ class VMXTemplate 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 static function timestamp($ts = 0, $format = 0) { @@ -628,7 +645,7 @@ class VMXTemplate { // TS_UNIX or Epoch 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)) { @@ -804,16 +821,17 @@ class VMXTemplateParser var $tokens, $tokpos, $tokline, $ptr; // 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_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_mul: exp_neg | exp_neg '*' ops_mul | exp_neg '/' ops_mul | exp_neg '%' ops_mul static $ops = array( - 'or' => array(array('||', '$or', '$xor'), 'and', true), - 'and' => array(array('&&', '$and'), 'eq', true), + 'and' => array(array('&&', '$and'), 'bitand', true), + 'bitand' => array(array('&'), 'eq', true), 'eq' => array(array('==', '!='), 'cmp', false), 'cmp' => array(array('<', '>', '<=', '>='), 'add', false), 'add' => array(array('+', '-'), 'mul', true), @@ -1093,8 +1111,8 @@ class VMXTemplateParser } $text = "Unexpected $tok, expected "; if (count($expected) > 1) - $text .= "one of '"; - $text .= implode("', '", $expected)."'"; + $text .= "one of "; + $text .= "'".implode("', '", $expected)."'"; $this->raise($text); } @@ -1200,10 +1218,23 @@ class VMXTemplateParser try { // Try to parse from here, skip invalid parts - $r = $this->$handler(); - $this->consume($this->eod); - // Add newline count from code fragment - $this->lineno += substr_count($this->code, "\n", $pos, $this->pos-$pos); + if ($this->tok() == '#') + { + // Comment! + $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) { @@ -1502,6 +1533,7 @@ $varref = array_pop(\$stack);"; if ($this->tok() == ',') $this->ptr++; } + $this->ptr++; } $code = false; if ($this->tok() == '=') @@ -1842,7 +1874,7 @@ $varref_index = \$stack[count(\$stack)-1]++;"; } $r = "\$this->parent->call_block($parts[0], $args, \"".addslashes($this->errorinfo())."\")"; } - if (count($parts) == 1) + elseif (count($parts) == 1) { $fn = strtolower($parts[0]); 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":"").")"; } /* удаление "небезопасных" HTML-тегов */ + /* TODO: м.б исправлять некорректную разметку? */ function function_strip_unsafe($e) { return "strip_tags($e, self::\$safe_tags)"; } /* заменить \n на
      */ @@ -2036,10 +2069,9 @@ $varref_index = \$stack[count(\$stack)-1]++;"; } /* strftime */ - function function_strftime($fmt, $date, $time = '') + function function_strftime($fmt, $date) { - $e = $time ? "($date).' '.($time)" : $date; - return "strftime($fmt, self::timestamp($e))"; + return "strftime($fmt, self::timestamp($date))"; } /* ограничение длины строки $maxlen символами на границе пробелов и добавление '...', если что. */ @@ -2050,6 +2082,12 @@ $varref_index = \$stack[count(\$stack)-1]++;"; 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)"; + } + /** Массивы и хеши **/ /* создание хеша */ diff --git a/olap.php b/olap.php index abc22e8..b2c8a87 100644 --- a/olap.php +++ b/olap.php @@ -343,7 +343,7 @@ class OLAP } $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['groups'] = $this->tpl_jsgraphs($vars['graphs']); @@ -682,7 +682,7 @@ class OLAP $i = count($tdhead); foreach ($cells as &$v) { - if (!$rows[$i]) + if (empty($rows[$i])) $rows[$i] = array(); $rows[$i] = array_merge($rows[$i], $v); $i++; diff --git a/templates/default/admin_olap.tpl b/templates/default/admin_olap.tpl index 71fe6ef..7dfd598 100644 --- a/templates/default/admin_olap.tpl +++ b/templates/default/admin_olap.tpl @@ -236,7 +236,7 @@ function doPlot(logarithmic) - +

      {d.field} ({lc d.func}): {d.value}

      @@ -271,7 +271,7 @@ function doPlot(logarithmic) - +

      {d.field} ({lc d.func}): {d.value}