Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Db/AbstractAdapter.php
<?php

namespace XF\Db;

use function
array_key_exists, gettype, in_array, intval, is_array, strlen, strval;

abstract class
AbstractAdapter
{
    const
ALLOW_DEADLOCK_RERUN = 1;

    protected
$statementClass;

    protected
$config;

    protected
$fullUnicode;

    protected
$inTransaction = false;
    protected
$savePointCounter = 0;
    protected
$savePoints = [];

    protected
$queryCount = 0;

    protected
$logQueries = false;
    protected
$logSimpleOnly = null;
    protected
$queryLog = [];

    protected
$ignoreLegacyTableWriteError = false;

    public static
$legacyTables = ['xf_liked_content'];

   
/**
     * @var SchemaManager
     */
   
protected $schemaManager;

    abstract protected function
getStatementClass();
    abstract protected function
_rawQuery($query);
    abstract protected function
standardizeConfig(array $config);

   
/**
     * @return mixed
     */
   
abstract public function getConnection();

    abstract public function
isConnected();
    abstract public function
ping();
    abstract public function
lastInsertId();
    abstract public function
getServerVersion();
    abstract public function
getConnectionStats();
    abstract public function
escapeString($string);
    abstract public function
getDefaultTableConfig();

    public function
__construct(array $config, $fullUnicode = false)
    {
       
$this->config = $config;
       
$this->fullUnicode = $fullUnicode;
       
$this->statementClass = $this->getStatementClass();
    }

    public function
connect()
    {
       
$this->getConnection();
        return
true;
    }

    public function
getConnectionForQuery($query)
    {
        return
$this->getConnection();
    }

    public function
closeConnection()
    {
        return;
    }

   
/**
     * @param $query
     * @param array $params
     *
     * @return AbstractStatement
     * @throws Exception
     */
   
public function query($query, $params = [])
    {
       
$this->connect();

        if (!empty(
$this->config['tablePrefix']))
        {
           
$query = $this->prependPrefixToTables($this->config['tablePrefix'], $query);
        }

       
$class = $this->statementClass;

       
/** @var AbstractStatement $statement */
       
$statement = new $class($this, $query, $params);
       
$statement->execute();

        return
$statement;
    }

    protected function
prependPrefixToTables($prefix, $query)
    {
        if (
$prefix == '')
        {
            return
$query;
        }

        if (
in_array('noPrefix', $this->getModifiersFromQuery($query)))
        {
            return
$query;
        }

        return
preg_replace('/((?:\s|^)(?:UPDATE|INTO|FROM|JOIN|STRAIGHT_JOIN)\s+)([a-z0-9_-]+(?:\s|$))/siU', '${1}' . $prefix . '${2}${3}', $query);
    }

    protected function
getModifiersFromQuery(&$query)
    {
       
$modifiers = [];

       
// strip any leading comments then search ones that start with XFDB= to find modifiers
       
if (preg_match('#^((\s*(--[^\n]*\n|\\#[^\n]*\n|/\*.*?\*/))+)\s*#si', $query, $match))
        {
           
$query = substr($query, strlen($match[0]));

           
preg_match_all(
               
'#--([^\n]*)\n|\\#([^\n]*)\n|/\*(.*?)\*/#',
               
$match[1],
               
$comments,
               
PREG_SET_ORDER
           
);

            foreach (
$comments AS $comment)
            {
               
$content = trim($comment[1] ?: $comment[2] ?: $comment[3] ?: '');
                if (
substr($content, 0, 5) == 'XFDB=' && strlen($content) > 5)
                {
                   
$modifiers = array_merge($modifiers, explode(',', substr($content, 5)));
                }
            }
        }

        return
$modifiers;
    }

    public function
fetchRow($query, $params = [])
    {
        return
$this->query($query, $params)->fetch();
    }

    public function
fetchOne($query, $params = [], $column = 0)
    {
        return
$this->query($query, $params)->fetchColumn($column);
    }

    public function
fetchAll($query, $params = [])
    {
        return
$this->query($query, $params)->fetchAll();
    }

    public function
fetchAllNum($query, $params = [])
    {
        return
$this->query($query, $params)->fetchAllNum();
    }

    public function
fetchAllKeyed($query, $key, $params = [])
    {
        return
$this->query($query, $params)->fetchAllKeyed($key);
    }

    public function
fetchAllColumn($query, $params = [], $column = 0)
    {
        return
$this->query($query, $params)->fetchAllColumn($column);
    }

    public function
fetchPairs($query, $params = [])
    {
        return
$this->query($query, $params)->fetchPairs();
    }

   
/**
     * @param $query
     *
     * @return mixed
     */
   
public function rawQuery($query)
    {
       
$this->logQueryExecution($query);
       
$res = $this->_rawQuery($query);
       
$this->logQueryCompletion();

        return
$res;
    }

    public function
insert($table, array $rawValues, $replaceInto = false, $onDupe = false, $modifier = '')
    {
        if (!
$rawValues)
        {
            throw new \
InvalidArgumentException('Values must be provided to insert');
        }

       
$cols = [];
       
$sqlValues = [];
       
$bind = [];
        foreach (
$rawValues AS $key => $value)
        {
           
$cols[] = "`$key`";
           
$bind[] = $value;
           
$sqlValues[] = '?';
        }

       
$keyword = ($replaceInto ? 'REPLACE' : 'INSERT');
        if (
$replaceInto)
        {
           
$onDupe = false;
        }

        try
        {
           
$res = $this->query(
               
"$keyword $modifier INTO `$table` (" . implode(', ', $cols) . ') VALUES '
               
. '(' . implode(', ', $sqlValues) . ')'
               
. ($onDupe ? " ON DUPLICATE KEY UPDATE $onDupe" : ''),
               
$bind
           
);
            return
$res->rowsAffected();
        }
        catch (
Exception $e)
        {
            return
$this->processDbWriteException($table, $e);
        }
    }

    public function
insertBulk($table, array $rows, $replaceInto = false, $onDupe = false, $modifier = '')
    {
        if (!
$rows)
        {
            throw new \
InvalidArgumentException('Rows must be provided to bulk insert');
        }

       
$firstRow = reset($rows);
       
$cols = array_keys($firstRow);

       
$rowSql = [];
        foreach (
$rows AS $row)
        {
           
$values = [];
            foreach (
$cols AS $col)
            {
                if (!
array_key_exists($col, $row))
                {
                    throw new \
InvalidArgumentException("Row missing column $col in bulk insert");
                }

               
$values[] = $this->quote($row[$col]);
            }

           
$rowSql[] = '(' . implode(',', $values) . ')';
        }

        foreach (
$cols AS &$col)
        {
           
$col = "`$col`";
        }

       
$keyword = ($replaceInto ? 'REPLACE' : 'INSERT');
        if (
$replaceInto)
        {
           
$onDupe = false;
        }

        try
        {
           
$res = $this->query(
               
"$keyword $modifier INTO `$table` (" . implode(', ', $cols) . ') VALUES '
               
. implode(",\n", $rowSql)
                . (
$onDupe ? " ON DUPLICATE KEY UPDATE $onDupe" : '')
            );
            return
$res->rowsAffected();
        }
        catch (
Exception $e)
        {
            return
$this->processDbWriteException($table, $e);
        }
    }

    public function
delete($table, $where, $params = [], $modifier = '', $order = '', $limit = 0)
    {
        try
        {
           
$res = $this->query(
               
"DELETE $modifier FROM `$table` WHERE " . ($where ? $where : '1=1')
                . (
$order ? " ORDER BY $order" : '')
                . (
$limit ? ' LIMIT ' . intval($limit) : ''),
               
$params
           
);
            return
$res->rowsAffected();
        }
        catch (
Exception $e)
        {
            return
$this->processDbWriteException($table, $e);
        }
    }

    public function
update($table, array $cols, $where, $params = [], $modifier = '', $order = '', $limit = 0)
    {
        if (!
$cols)
        {
            return
0;
        }

       
$sqlValues = [];
       
$bind = [];
        foreach (
$cols AS $col => $value)
        {
           
$bind[] = $value;
           
$sqlValues[] = "`$col` = ?";
        }

       
$bind = array_merge($bind, is_array($params) ? $params : [$params]);

        try
        {
           
$res = $this->query(
               
"UPDATE $modifier `$table` SET " . implode(', ', $sqlValues)
                .
' WHERE ' . ($where ? $where : '1=1')
                . (
$order ? " ORDER BY $order" : '')
                . (
$limit ? ' LIMIT ' . intval($limit) : ''),
               
$bind
           
);
            return
$res->rowsAffected();
        }
        catch (
Exception $e)
        {
            return
$this->processDbWriteException($table, $e);
        }
    }

    public function
emptyTable($table)
    {
       
$method = $this->config['emptyMethod'] ?? 'TRUNCATE';
        switch (
strtoupper($method))
        {
            case
'TRUNCATE':
               
$query = "TRUNCATE TABLE `$table`";
                break;

            case
'DELETE':
               
$query = "DELETE FROM `$table`";
                break;

            default:
                throw new \
InvalidArgumentException("Unknown emptyMethod '$method'.");
        }

        try
        {
           
$res = $this->query($query);
            return
$res->rowsAffected();
        }
        catch (
Exception $e)
        {
            return
$this->processDbWriteException($table, $e);
        }
    }

    protected function
processDbWriteException($table, Exception $e)
    {
        if (
$this->ignoreLegacyTableWriteError && in_array($table, self::$legacyTables))
        {
            \
XF::logException($e, false, "Ignored write to legacy table $table. Code update required. ");
            return
0;
        }

        throw
$e;
    }

    public function
beginTransaction()
    {
       
$this->connect();

        if (!
$this->inTransaction)
        {
           
$this->rawQuery('BEGIN');
           
$this->inTransaction = true;
        }
        else
        {
           
$savepoint = 'save' . ++$this->savePointCounter;
           
$this->rawQuery("SAVEPOINT $savepoint");

           
$this->savePoints[] = $savepoint;
        }
    }

    public function
commit()
    {
        if (
$this->inTransaction)
        {
            if (
$this->savePoints)
            {
               
$this->rawQuery('RELEASE SAVEPOINT ' . array_pop($this->savePoints));
            }
            else
            {
               
$this->rawQuery('COMMIT');
               
$this->inTransaction = false;
            }
        }
    }

    public function
commitAll()
    {
        if (
$this->inTransaction)
        {
           
$this->rawQuery('COMMIT');

           
$this->inTransaction = false;
           
$this->savePoints = [];
        }
    }

    public function
rollback()
    {
        if (
$this->inTransaction)
        {
            if (
$this->savePoints)
            {
               
$this->rawQuery('ROLLBACK TO SAVEPOINT ' . array_pop($this->savePoints));
            }
            else
            {
               
$this->rawQuery('ROLLBACK');
               
$this->inTransaction = false;
            }
        }
    }

    public function
rollbackAll()
    {
        if (
$this->inTransaction)
        {
           
$this->rawQuery('ROLLBACK');

           
$this->inTransaction = false;
           
$this->savePoints = [];
        }
    }

    public function
executeTransaction(callable $execute, $options = 0)
    {
       
$startedInTransaction = $this->inTransaction;

       
$this->beginTransaction();

        try
        {
           
$result = $execute($this);
        }
        catch (\
XF\Db\DeadlockException $e)
        {
           
$this->rollback();

            if (!
$startedInTransaction && $options & self::ALLOW_DEADLOCK_RERUN)
            {
               
// deadlock detected, try rerunning once
               
$result = $this->executeTransaction($execute, $options & ~self::ALLOW_DEADLOCK_RERUN);
            }
            else
            {
                throw
$e;
            }
        }
        catch (\
Exception $e)
        {
           
$this->rollback();
            throw
$e;
        }

       
$this->commit();

        return
$result;
    }

    public function
inTransaction()
    {
        return
$this->inTransaction;
    }

    public function
quote($data, $type = null)
    {
        if (
is_array($data))
        {
           
$output = [];
            foreach (
$data AS $value)
            {
               
$output[] = $this->quote($value);
            }
            return
implode(', ', $output);
        }

        if (!
$type)
        {
           
$type = gettype($data);
        }

        switch (
strtolower($type))
        {
            case
'integer':
                return
strval(floor($data));
            case
'double':
                return
strval($data + 0);
            case
'boolean':
                return (
$data ? '1' : '0');
            case
'null':
                return
'NULL';

            default:
                return
"'" . $this->escapeString($data) . "'";
        }
    }

    public function
escapeLike($data, $format = null)
    {
        if (
is_array($data))
        {
           
$output = [];
            foreach (
$data AS $value)
            {
               
$output[] = $this->escapeLike($value, $format);
            }
            return
implode(', ', $output);
        }

       
$data = strval($data);
       
$data = strtr($data, [
           
'_' => '\\_',
           
'%' => '\\%'
       
]);
        if (
$format)
        {
           
$data = str_replace('?', $data, $format);
        }

        return
$data;
    }

    public function
limit($query, $amount, $offset = 0)
    {
       
$offset = max(0, intval($offset));

        if (
$amount === null)
        {
            if (!
$offset)
            {
               
// no limit
               
return $query;
            }

           
// no amount limit, but there's an offset
           
$amount = 1000000;
        }
       
$amount = max(1, intval($amount));

        return
"$query\nLIMIT $amount" . ($offset ? " OFFSET $offset" : '');
    }

    public function
logQueries($value, $simpleOnly = null)
    {
       
$this->logQueries = (bool)$value;
       
$this->logSimpleOnly = $simpleOnly;
    }

    public function
getUtf8Type()
    {
        return
$this->fullUnicode ? 'utf8mb4' : 'utf8';
    }

    public function
logSimpleOnly($value = true)
    {
       
$this->logSimpleOnly = $value;
    }

    public function
areQueriesLogged()
    {
        return
$this->logQueries;
    }

    public function
logQueryExecution($query, array $params = [])
    {
       
$this->queryCount++;

        if (
$this->logQueries)
        {
            if (
$this->logSimpleOnly)
            {
               
$trace = null;
            }
            else
            {
               
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
               
array_shift($trace);
            }

           
$this->queryLog[$this->queryCount] = [
               
'query' => $query,
               
'params' => $this->logSimpleOnly ? null : $params,
               
'start' => microtime(true),
               
'trace' => $trace
           
];

            if (
$this->queryCount >= 150 && $this->logSimpleOnly === null)
            {
               
// we haven't specified that we want full details, so switch to reduce memory usage
               
$this->logSimpleOnly = true;
            }
        }

        return
$this->queryCount;
    }

    public function
logQueryStage($stage, $queryId = null)
    {
        if (!
$this->logQueries)
        {
            return;
        }

        if (!
$queryId)
        {
           
$queryId = $this->queryCount;
        }
        if (!isset(
$this->queryLog[$queryId]))
        {
            return;
        }

       
$this->queryLog[$queryId][$stage] = microtime(true);
    }

    public function
logQueryCompletion($queryId = null)
    {
        if (!
$this->logQueries)
        {
            return;
        }

       
$this->logQueryStage('complete', $queryId);
    }

    public function
getQueryCount()
    {
        return
$this->queryCount;
    }

    public function
getQueryLog()
    {
        return
$this->queryLog;
    }

    public function
ignoreLegacyTableWriteError($ignore)
    {
       
$this->ignoreLegacyTableWriteError = (bool)$ignore;
    }

    public function
getIgnoreLegacyTableWriteError()
    {
        return
$this->ignoreLegacyTableWriteError;
    }

    public function
getSchemaManager()
    {
        if (
$this->schemaManager === null)
        {
           
$this->schemaManager = new SchemaManager($this);
        }

        return
$this->schemaManager;
    }

    public function
__sleep()
    {
        throw new \
LogicException('Instances of ' . __CLASS__ . ' cannot be serialized or unserialized');
    }

    public function
__wakeup()
    {
        throw new \
LogicException('Instances of ' . __CLASS__ . ' cannot be serialized or unserialized');
    }
}