<?php
/**
* TorrentPier – Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2018 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Legacy;
use mysqli_result;
/**
* Class SqlDb
* @package TorrentPier\Legacy
*/
class SqlDb
{
public $cfg = [];
public $cfg_keys = ['dbhost', 'dbname', 'dbuser', 'dbpasswd', 'charset', 'persist'];
private $link;
public $result;
public $db_server = '';
public $selected_db;
public $inited = false;
public $locked = false;
public $locks = [];
public $num_queries = 0;
public $sql_starttime = 0;
public $sql_inittime = 0;
public $sql_timetotal = 0;
public $cur_query_time = 0;
public $slow_time = 0;
public $dbg = [];
public $dbg_id = 0;
public $dbg_enabled = false;
public $cur_query;
public $do_explain = false;
public $explain_hold = '';
public $explain_out = '';
public $shutdown = [];
public $DBS = [];
/**
* sql_db constructor.
* @param $cfg_values
*/
public function __construct($cfg_values)
{
global $DBS;
$this->cfg = array_combine($this->cfg_keys, $cfg_values);
$this->dbg_enabled = (sql_dbg_enabled() || !empty($_COOKIE['explain']));
$this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain']));
$this->slow_time = SQL_SLOW_QUERY_TIME;
// ссылки на глобальные переменные (для включения логов сразу на всех серверах, подсчета общего количества запросов и т.д.)
$this->DBS['log_file'] =& $DBS->log_file;
$this->DBS['log_counter'] =& $DBS->log_counter;
$this->DBS['num_queries'] =& $DBS->num_queries;
$this->DBS['sql_inittime'] =& $DBS->sql_inittime;
$this->DBS['sql_timetotal'] =& $DBS->sql_timetotal;
}
/**
* Initialize connection
*/
public function init()
{
// Connect to server
$this->connect();
// Set charset
if ($this->cfg['charset'] && !mysqli_set_charset($this->link, $this->cfg['charset'])) {
if (!$this->sql_query("SET NAMES {$this->cfg['charset']}")) {
die("Could not set charset {$this->cfg['charset']}");
}
}
$this->inited = true;
$this->num_queries = 0;
$this->sql_inittime = $this->sql_timetotal;
$this->DBS['sql_inittime'] += $this->sql_inittime;
}
/**
* Open connection
*/
public function connect()
{
$this->cur_query = $this->dbg_enabled ? "connect to: {$this->cfg['dbhost']}" : 'connect';
$this->debug('start');
$p = ((bool)$this->cfg['persist']) ? 'p:' : '';
$this->link = mysqli_connect($p . $this->cfg['dbhost'], $this->cfg['dbuser'], $this->cfg['dbpasswd'], $this->cfg['dbname']);
$this->selected_db = $this->cfg['dbname'];
if (mysqli_connect_error()) {
$server = DBG_USER ? $this->cfg['dbhost'] : '';
header('HTTP/1.0 503 Service Unavailable');
bb_log(' ', "db_err/connect_failed_{$this->cfg['dbhost']}");
die("Could not connect to mysql server $server");
}
register_shutdown_function([&$this, 'close']);
$this->debug('stop');
$this->cur_query = null;
}
/**
* Base query method
*
* @param $query
*
* @return bool|mysqli_result|null
*/
public function sql_query($query)
{
if (!$this->link) {
$this->init();
}
if (\is_array($query)) {
$query = $this->build_sql($query);
}
if (SQL_PREPEND_SRC_COMM) {
$query = '/* ' . $this->debug_find_source() . ' */ ' . $query;
}
$this->cur_query = $query;
$this->debug('start');
if (!$this->result = mysqli_query($this->link, $query)) {
$this->log_error();
}
$this->debug('stop');
$this->cur_query = null;
if ($this->inited) {
$this->num_queries++;
$this->DBS['num_queries']++;
}
return $this->result;
}
/**
* Execute query WRAPPER (with error handling)
*
* @param $query
*
* @return bool|mysqli_result|null
*/
public function query($query)
{
if (!$result = $this->sql_query($query)) {
$this->trigger_error();
}
return $result;
}
/**
* Return number of rows
*
* @param bool $result
*
* @return bool|int
*/
public function num_rows($result = false)
{
$num_rows = false;
if ($result or $result = $this->result) {
$num_rows = $result instanceof mysqli_result ? mysqli_num_rows($result) : false;
}
return $num_rows;
}
/**
* Return number of affected rows
*
* @return int
*/
public function affected_rows()
{
return mysqli_affected_rows($this->link);
}
/**
* Fetch current field
*
* @param $field
* @param int $rownum
* @param int $query_id
*
* @return bool
*/
public function sql_fetchfield($field, $rownum = -1, $query_id = 0)
{
if (!$query_id) {
$query_id = $this->query_result;
}
if ($query_id) {
if ($rownum > -1) {
$result = $this->sql_result($query_id, $rownum, $field);
} else {
if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) {
if ($this->sql_fetchrow()) {
$result = $this->row[$query_id][$field];
}
} else {
if ($this->rowset[$query_id]) {
$result = $this->rowset[$query_id][0][$field];
} elseif ($this->row[$query_id]) {
$result = $this->row[$query_id][$field];
}
}
}
return $result;
}
return false;
}
/**
* @param mysqli_result $res
* @param $row
* @param int $field
*
* @return mixed
*/
private function sql_result(mysqli_result $res, $row, $field = 0)
{
$res->data_seek($row);
$dataRow = $res->fetch_array();
return $dataRow[$field];
}
/**
* Fetch current row
*
* @param $result
* @param string $field_name
*
* @return array|bool|null
*/
public function sql_fetchrow($result, $field_name = '')
{
$row = mysqli_fetch_assoc($result);
if ($field_name) {
return $row[$field_name] ?? false;
}
return $row;
}
/**
* Alias of sql_fetchrow()
* @param $result
*
* @return array|bool|null
*/
public function fetch_next($result)
{
return $this->sql_fetchrow($result);
}
/**
* Fetch row WRAPPER (with error handling)
* @param $query
* @param string $field_name
*
* @return array|bool|null
*/
public function fetch_row($query, $field_name = '')
{
if (!$result = $this->sql_query($query)) {
$this->trigger_error();
}
return $this->sql_fetchrow($result, $field_name);
}
/**
* Fetch all rows
*
* @param $result
* @param string $field_name
*
* @return array
*/
public function sql_fetchrowset($result, $field_name = '')
{
$rowset = [];
while ($row = mysqli_fetch_assoc($result)) {
$rowset[] = $field_name ? $row[$field_name] : $row;
}
return $rowset;
}
/**
* Fetch all rows WRAPPER (with error handling)
*
* @param $query
* @param string $field_name
*
* @return array
*/
public function fetch_rowset($query, $field_name = '')
{
if (!$result = $this->sql_query($query)) {
$this->trigger_error();
}
return $this->sql_fetchrowset($result, $field_name);
}
/**
* Fetch all rows WRAPPER (with error handling)
*
* @param $query
* @param string $field_name
*
* @return array
*/
public function fetch_all($query, $field_name = '')
{
if (!$result = $this->sql_query($query)) {
$this->trigger_error();
}
return $this->sql_fetchrowset($result, $field_name);
}
/**
* Get last inserted id after insert statement
*
* @return int|string
*/
public function sql_nextid()
{
return mysqli_insert_id($this->link);
}
/**
* Free sql result
*
* @param bool $result
*/
public function sql_freeresult($result = false)
{
if ($result or $result = $this->result) {
if ($result instanceof mysqli_result) {
mysqli_free_result($result);
}
}
$this->result = null;
}
/**
* Escape data used in sql query
*
* @param $v
* @param bool $check_type
* @param bool $dont_escape
*
* @return string
*/
public function escape($v, $check_type = false, $dont_escape = false)
{
if ($dont_escape) {
return $v;
}
if (!$check_type) {
return $this->escape_string($v);
}
switch (true) {
case \is_string($v):
return "'" . $this->escape_string($v) . "'";
case \is_int($v):
return (string)$v;
case \is_bool($v):
return ($v) ? '1' : '0';
case \is_float($v):
return "'$v'";
case null === $v:
return 'NULL';
}
// if $v has unsuitable type
$this->trigger_error(__FUNCTION__ . ' - wrong params');
}
/**
* Escape string
*
* @param $str
*
* @return string
*/
public function escape_string($str)
{
if (!$this->link) {
$this->init();
}
return mysqli_real_escape_string($this->link, $str);
}
/**
* Build SQL statement from array.
* Possible $query_type values: INSERT, INSERT_SELECT, MULTI_INSERT, UPDATE, SELECT
*
* @param $query_type
* @param $input_ary
* @param bool $data_already_escaped
* @param bool $check_data_type_in_escape
*
* @return string
*/
public function build_array($query_type, $input_ary, $data_already_escaped = false, $check_data_type_in_escape = true)
{
$fields = $values = $ary = $query = [];
$dont_escape = $data_already_escaped;
$check_type = $check_data_type_in_escape;
if (empty($input_ary) || !\is_array($input_ary)) {
$this->trigger_error(__FUNCTION__ . ' - wrong params: $input_ary');
}
if ($query_type == 'INSERT') {
foreach ($input_ary as $field => $val) {
$fields[] = $field;
$values[] = $this->escape($val, $check_type, $dont_escape);
}
$fields = implode(', ', $fields);
$values = implode(', ', $values);
$query = "($fields)\nVALUES\n($values)";
} elseif ($query_type == 'INSERT_SELECT') {
foreach ($input_ary as $field => $val) {
$fields[] = $field;
$values[] = $this->escape($val, $check_type, $dont_escape);
}
$fields = implode(', ', $fields);
$values = implode(', ', $values);
$query = "($fields)\nSELECT\n$values";
} elseif ($query_type == 'MULTI_INSERT') {
foreach ($input_ary as $id => $sql_ary) {
foreach ($sql_ary as $field => $val) {
$values[] = $this->escape($val, $check_type, $dont_escape);
}
$ary[] = '(' . implode(', ', $values) . ')';
$values = [];
}
$fields = implode(', ', array_keys($input_ary[0]));
$values = implode(",\n", $ary);
$query = "($fields)\nVALUES\n$values";
} elseif ($query_type == 'SELECT' || $query_type == 'UPDATE') {
foreach ($input_ary as $field => $val) {
$ary[] = "$field = " . $this->escape($val, $check_type, $dont_escape);
}
$glue = ($query_type == 'SELECT') ? "\nAND " : ",\n";
$query = implode($glue, $ary);
}
if (!$query) {
bb_die('<pre><b>' . __FUNCTION__ . "</b>: Wrong params for <b>$query_type</b> query type\n\n\$input_ary:\n\n" . htmlCHR(print_r($input_ary, true)) . '</pre>');
}
return "\n" . $query . "\n";
}
/**
* @return array
*/
public function get_empty_sql_array()
{
return [
'SELECT' => [],
'select_options' => [],
'FROM' => [],
'INNER JOIN' => [],
'LEFT JOIN' => [],
'WHERE' => [],
'GROUP BY' => [],
'HAVING' => [],
'ORDER BY' => [],
'LIMIT' => [],
];
}
/**
* @param $sql_ary
* @return string
*/
public function build_sql($sql_ary)
{
$sql = '';
array_deep($sql_ary, 'array_unique', false, true);
foreach ($sql_ary as $clause => $ary) {
switch ($clause) {
case 'SELECT':
$sql .= ($ary) ? ' SELECT ' . implode(' ', $sql_ary['select_options']) . ' ' . implode(', ', $ary) : '';
break;
case 'FROM':
$sql .= ($ary) ? ' FROM ' . implode(', ', $ary) : '';
break;
case 'INNER JOIN':
$sql .= ($ary) ? ' INNER JOIN ' . implode(' INNER JOIN ', $ary) : '';
break;
case 'LEFT JOIN':
$sql .= ($ary) ? ' LEFT JOIN ' . implode(' LEFT JOIN ', $ary) : '';
break;
case 'WHERE':
$sql .= ($ary) ? ' WHERE ' . implode(' AND ', $ary) : '';
break;
case 'GROUP BY':
$sql .= ($ary) ? ' GROUP BY ' . implode(', ', $ary) : '';
break;
case 'HAVING':
$sql .= ($ary) ? ' HAVING ' . implode(' AND ', $ary) : '';
break;
case 'ORDER BY':
$sql .= ($ary) ? ' ORDER BY ' . implode(', ', $ary) : '';
break;
case 'LIMIT':
$sql .= ($ary) ? ' LIMIT ' . implode(', ', $ary) : '';
break;
}
}
return trim($sql);
}
/**
* Return sql error array
*
* @return array
*/
public function sql_error()
{
if ($this->link) {
return ['code' => mysqli_errno($this->link), 'message' => mysqli_error($this->link)];
}
return ['code' => '', 'message' => 'not connected'];
}
/**
* Close sql connection
*/
public function close()
{
if ($this->link) {
$this->unlock();
if (!empty($this->locks)) {
foreach ($this->locks as $name => $void) {
$this->release_lock($name);
}
}
$this->exec_shutdown_queries();
mysqli_close($this->link);
}
$this->link = $this->selected_db = null;
}
/**
* Add shutdown query
*
* @param $sql
*/
public function add_shutdown_query($sql)
{
$this->shutdown['__sql'][] = $sql;
}
/**
* Exec shutdown queries
*/
public function exec_shutdown_queries()
{
if (empty($this->shutdown)) {
return;
}
if (!empty($this->shutdown['post_html'])) {
$post_html_sql = $this->build_array('MULTI_INSERT', $this->shutdown['post_html']);
$this->query("REPLACE INTO " . BB_POSTS_HTML . " $post_html_sql");
}
if (!empty($this->shutdown['__sql'])) {
foreach ($this->shutdown['__sql'] as $sql) {
$this->query($sql);
}
}
}
/**
* Lock tables
*
* @param $tables
* @param string $lock_type
*
* @return bool|mysqli_result|null
*/
public function lock($tables, $lock_type = 'WRITE')
{
$tables_sql = [];
foreach ((array)$tables as $table_name) {
$tables_sql[] = "$table_name $lock_type";
}
if ($tables_sql = implode(', ', $tables_sql)) {
$this->locked = $this->sql_query("LOCK TABLES $tables_sql");
}
return $this->locked;
}
/**
* Unlock tables
*
* @return bool
*/
public function unlock()
{
if ($this->locked && $this->sql_query("UNLOCK TABLES")) {
$this->locked = false;
}
return !$this->locked;
}
/**
* Obtain user level lock
*
* @param $name
* @param int $timeout
*
* @return mixed
*/
public function get_lock($name, $timeout = 0)
{
$lock_name = $this->get_lock_name($name);
$timeout = (int)$timeout;
$row = $this->fetch_row("SELECT GET_LOCK('$lock_name', $timeout) AS lock_result");
if ($row['lock_result']) {
$this->locks[$name] = true;
}
return $row['lock_result'];
}
/**
* Obtain user level lock status
*
* @param $name
*
* @return mixed
*/
public function release_lock($name)
{
$lock_name = $this->get_lock_name($name);
$row = $this->fetch_row("SELECT RELEASE_LOCK('$lock_name') AS lock_result");
if ($row['lock_result']) {
unset($this->locks[$name]);
}
return $row['lock_result'];
}
/**
* Release user level lock
*
* @param $name
*
* @return mixed
*/
public function is_free_lock($name)
{
$lock_name = $this->get_lock_name($name);
$row = $this->fetch_row("SELECT IS_FREE_LOCK('$lock_name') AS lock_result");
return $row['lock_result'];
}
/**
* Make per db unique lock name
*
* @param $name
*
* @return string
*/
public function get_lock_name($name)
{
if (!$this->selected_db) {
$this->init();
}
return "{$this->selected_db}_{$name}";
}
/**
* Get info about last query
*
* @return mixed
*/
public function query_info()
{
$info = [];
if ($num = $this->num_rows($this->result)) {
$info[] = "$num rows";
}
if ($this->link and $ext = mysqli_info($this->link)) {
$info[] = (string)$ext;
} elseif (!$num && ($aff = $this->affected_rows($this->result) and $aff != -1)) {
$info[] = "$aff rows";
}
return str_compact(implode(', ', $info));
}
/**
* Get server version
*
* @return mixed
*/
public function server_version()
{
preg_match('#^(\d+\.\d+\.\d+).*#', mysqli_get_server_info($this->link), $m);
return $m[1];
}
/**
* Set slow query marker for xx seconds.
* This will disable counting other queries as "slow" during this time.
*
* @param int $ignoring_time
* @param int $new_priority
*/
public function expect_slow_query($ignoring_time = 60, $new_priority = 10)
{
if ($old_priority = CACHE('bb_cache')->get('dont_log_slow_query')) {
if ($old_priority > $new_priority) {
return;
}
}
if (!\defined('IN_FIRST_SLOW_QUERY')) {
\define('IN_FIRST_SLOW_QUERY', true);
}
CACHE('bb_cache')->set('dont_log_slow_query', $new_priority, $ignoring_time);
}
/**
* Store debug info
*
* @param $mode
*/
public function debug($mode)
{
if (!SQL_DEBUG) {
return;
}
$id =& $this->dbg_id;
$dbg =& $this->dbg[$id];
if ($mode == 'start') {
if (SQL_CALC_QUERY_TIME || SQL_LOG_SLOW_QUERIES) {
$this->sql_starttime = utime();
}
if ($this->dbg_enabled) {
$dbg['sql'] = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $this->cur_query);
$dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
$dbg['time'] = '';
$dbg['info'] = '';
$dbg['mem_before'] = sys('mem');
}
if ($this->do_explain) {
$this->explain('start');
}
} elseif ($mode == 'stop') {
if (SQL_CALC_QUERY_TIME || SQL_LOG_SLOW_QUERIES) {
$this->cur_query_time = utime() - $this->sql_starttime;
$this->sql_timetotal += $this->cur_query_time;
$this->DBS['sql_timetotal'] += $this->cur_query_time;
if (SQL_LOG_SLOW_QUERIES && $this->cur_query_time > $this->slow_time) {
$this->log_slow_query();
}
}
if ($this->dbg_enabled) {
$dbg['time'] = utime() - $this->sql_starttime;
$dbg['info'] = $this->query_info();
$dbg['mem_after'] = sys('mem');
$id++;
}
if ($this->do_explain) {
$this->explain('stop');
}
// проверка установки $this->inited - для пропуска инициализационных запросов
if ($this->DBS['log_counter'] && $this->inited) {
$this->log_query($this->DBS['log_file']);
$this->DBS['log_counter']--;
}
}
}
/**
* Trigger error
*
* @param string $msg
*/
public function trigger_error($msg = 'DB Error')
{
if (error_reporting()) {
$msg .= ' [' . $this->debug_find_source() . ']';
trigger_error($msg, E_USER_ERROR);
}
}
/**
* Find caller source
*
* @param string $mode
*
* @return string
*/
public function debug_find_source($mode = '')
{
foreach (debug_backtrace() as $trace) {
if (!empty($trace['file']) && $trace['file'] !== __FILE__) {
switch ($mode) {
case 'file':
return $trace['file'];
case 'line':
return $trace['line'];
default:
return hide_bb_path($trace['file']) . '(' . $trace['line'] . ')';
}
}
}
return '';
}
/**
* Prepare for logging
* @param int $queries_count
* @param string $log_file
*/
public function log_next_query($queries_count = 1, $log_file = 'sql_queries')
{
$this->DBS['log_file'] = $log_file;
$this->DBS['log_counter'] = $queries_count;
}
/**
* Log query
*
* @param string $log_file
*/
public function log_query($log_file = 'sql_queries')
{
$q_time = ($this->cur_query_time >= 10) ? round($this->cur_query_time, 0) : sprintf('%.4f', $this->cur_query_time);
$msg = [];
$msg[] = round($this->sql_starttime);
$msg[] = date('m-d H:i:s', $this->sql_starttime);
$msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%-4s', round(sys('la'), 1));
$msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db_server;
$msg[] = short_query($this->cur_query);
$msg = implode(LOG_SEPR, $msg);
$msg .= ($info = $this->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' ';
$msg .= \defined('IN_CRON') ? 'cron' : basename($_SERVER['REQUEST_URI']);
bb_log($msg . LOG_LF, $log_file);
}
/**
* Log slow query
*
* @param string $log_file
*/
public function log_slow_query($log_file = 'sql_slow_bb')
{
if (!\defined('IN_FIRST_SLOW_QUERY') && CACHE('bb_cache')->get('dont_log_slow_query')) {
return;
}
$this->log_query($log_file);
}
/**
* Log error
*/
public function log_error()
{
if (!SQL_LOG_ERRORS) {
return;
}
$msg = [];
$err = $this->sql_error();
$msg[] = str_compact(sprintf('#%06d %s', $err['code'], $err['message']));
$msg[] = '';
$msg[] = str_compact($this->cur_query);
$msg[] = '';
$msg[] = 'Source : ' . $this->debug_find_source() . " :: $this->db_server.$this->selected_db";
$msg[] = 'IP : ' . @$_SERVER['REMOTE_ADDR'];
$msg[] = 'Date : ' . date('Y-m-d H:i:s');
$msg[] = 'Agent : ' . @$_SERVER['HTTP_USER_AGENT'];
$msg[] = 'Req_URI : ' . @$_SERVER['REQUEST_URI'];
$msg[] = 'Referer : ' . @$_SERVER['HTTP_REFERER'];
$msg[] = 'Method : ' . @$_SERVER['REQUEST_METHOD'];
$msg[] = 'PID : ' . sprintf('%05d', getmypid());
$msg[] = 'Request : ' . trim(print_r($_REQUEST, true)) . str_repeat('_', 78) . LOG_LF;
$msg[] = '';
bb_log($msg, 'sql_error_bb');
}
/**
* Explain queries
*
* @param $mode
* @param string $html_table
* @param string $row
*
* @return bool|string
*/
public function explain($mode, $html_table = '', $row = '')
{
$query = str_compact($this->cur_query);
// remove comments
$query = preg_replace('#(\s*)(/\*)(.*)(\*/)(\s*)#', '', $query);
switch ($mode) {
case 'start':
$this->explain_hold = '';
if (preg_match('#UPDATE ([a-z0-9_]+).*?WHERE(.*)/#', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
} elseif (preg_match('#DELETE FROM ([a-z0-9_]+).*?WHERE(.*)#s', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
}
if (0 === strpos($query, "SELECT")) {
$html_table = false;
if ($result = mysqli_query($this->link, "EXPLAIN $query")) {
while ($row = mysqli_fetch_assoc($result)) {
$html_table = $this->explain('add_explain_row', $html_table, $row);
}
}
if ($html_table) {
$this->explain_hold .= '</table>';
}
}
break;
case 'stop':
if (!$this->explain_hold) {
break;
}
$id = $this->dbg_id - 1;
$htid = 'expl-' . spl_object_hash($this->link) . '-' . $id;
$dbg = $this->dbg[$id];
$this->explain_out .= '
<table width="98%" cellpadding="0" cellspacing="0" class="bodyline row2 bCenter" style="border-bottom: 0;">
<tr>
<th style="height: 22px; cursor: pointer;" align="left"> ' . $dbg['src'] . ' [' . sprintf('%.4f', $dbg['time']) . ' s] <i>' . $dbg['info'] . '</i></th>
<th style="height: 22px; cursor: pointer;" align="right" title="Copy to clipboard" onclick="$.copyToClipboard( $(\'#' . $htid . '\').text() );">' . "$this->db_server.$this->selected_db" . ' :: Query #' . ($this->num_queries + 1) . ' </th>
</tr>
<tr><td colspan="2">' . $this->explain_hold . '</td></tr>
</table>
<div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . short_query($dbg['sql'], true) . ' </div></div>
<br />';
break;
case 'add_explain_row':
if (!$html_table && $row) {
$html_table = true;
$this->explain_hold .= '<table width="100%" cellpadding="3" cellspacing="1" class="bodyline" style="border-width: 0;"><tr>';
foreach (array_keys($row) as $val) {
$this->explain_hold .= '<td class="row3 gensmall" align="center"><b>' . $val . '</b></td>';
}
$this->explain_hold .= '</tr>';
}
$this->explain_hold .= '<tr>';
foreach (array_values($row) as $i => $val) {
$class = !($i % 2) ? 'row1' : 'row2';
$this->explain_hold .= '<td class="' . $class . ' gen">' . str_replace(["{$this->selected_db}.", ',', ';'], ['', ', ', ';<br />'], $val) . '</td>';
}
$this->explain_hold .= '</tr>';
return $html_table;
break;
case 'display':
echo '<a name="explain"></a><div class="med">' . $this->explain_out . '</div>';
break;
}
}
}