Backticking MySQL Entities - PullRequest
       25

Backticking MySQL Entities

0 голосов
/ 26 августа 2009

У меня есть следующий метод, который позволяет мне защитить сущности MySQL:

public function Tick($string)
{
    $string = explode('.', str_replace('`', '', $string));

    foreach ($string as $key => $value)
    {
        if ($value != '*')
        {
            $string[$key] = '`' . trim($value) . '`';
        }
    }

    return implode('.', $string);
}

Это работает довольно хорошо для использования, которое я использую.

Он защищает базу данных, таблицы, имена полей и даже оператор *, однако теперь я также хочу, чтобы он защищал вызовы функций, например:

AVG(database.employees.salary)

Должно стать:

AVG(`database`.`employees`.`salary`) and not `AVG(database`.`employees`.`salary)`

Как мне поступить об этом? Должен ли я использовать регулярные выражения?

Также, как я могу поддержать более продвинутые вещи, от:

MAX(AVG(database.table.field1), MAX(database.table.field2))

До:

MAX(AVG(`database`.`table`.`field1`), MAX(`database`.`table`.`field2`))

Пожалуйста, имейте в виду, что я хочу сделать этот метод максимально простым / быстрым, так как он в значительной степени повторяет все имена сущностей в моей базе данных.

Ответы [ 7 ]

4 голосов
/ 26 августа 2009

Если это цитирование частей оператора SQL, и они имеют только сложность, которую вы описали, RegEx - отличный подход. С другой стороны, если вам нужно сделать это для полных операторов SQL или просто для более сложных компонентов операторов (таких как «MAX (AVG (val), MAX (val2))»), вам потребуется токенизировать или проанализировать строка и иметь более глубокое понимание этого, чтобы сделать это цитирование точно.

Учитывая подход с регулярными выражениями, вам может оказаться проще разделить имя функции за один шаг, а затем использовать текущий код для цитирования имен базы данных / таблицы / столбца. Это может быть сделано в одном RE, но это будет сложнее, чтобы получить право.

В любом случае, я бы настоятельно рекомендовал написать несколько примеров модульных тестов. Фактически, это идеальная ситуация для этого подхода: написать тесты легко, у вас есть несколько существующих случаев, которые работают (которые вы не хотите разбивать), и вам нужно добавить еще один случай.

Ваш тест может начаться так же просто, как:

assert '`ticked`' == Tick('ticked');
assert '`table`.`ticked`' == Tick('table.ticked');
assert 'db`.`table`.`ticked`' == Tick('db.table.ticked');

А затем добавьте:

assert 'FN(`ticked`)' == Tick('FN(ticked)');
etc.
2 голосов
/ 03 сентября 2009

Используя тестовый пример ndp дал я создал регулярное выражение, чтобы сделать тяжелую работу за вас. Следующее регулярное выражение заменит все границы слов вокруг слов, за которыми не следует открывающая скобка.

\b(\w+)\b(?!\()

Функциональность Tick () будет реализована в PHP следующим образом:

function Tick($string) 
{
    return preg_replace( '/\b(\w+)\b(?!\()/', '`\1`', $string );
}
2 голосов
/ 01 сентября 2009

Обычно плохая идея передавать весь SQL в функцию. Таким образом, вы всегда найдете случай, когда он не работает, если вы не полностью проанализируете синтаксис SQL.

Поставьте галочки для имен на некотором предыдущем уровне абстракции, который составляет SQL.

2 голосов
/ 01 сентября 2009

Перед тем, как разбить строку на точки, проверьте, является ли последний символ круглой скобкой. Если это так, этот вызов является функцией.

<?php
$string = str_replace('`', '', $string)
$function = "";
if (substr($string,-1) == ")") {
  // Strip off function call first
  $opening = strpos($string, "(");
  $function = substr($string, 0, $opening+1);
  $string = substr($string, $opening+1, -1);
}

// Do your existing parsing to $string

if ($function == "") {
  // Put function back on string
  $string = $function . $string . ")";
}
?>

Если вам нужно охватить более сложные ситуации, такие как использование вложенных функций или несколько последовательных функций в одной переменной "$ string", это станет гораздо более сложной функцией, и вам лучше спросить себя, почему эти элементы Во-первых, неправильно отмечены и не нуждаются в дальнейшем разборе.

РЕДАКТИРОВАТЬ: Обновление для вложенных функций, в соответствии с исходной записи редактирования Чтобы вышеуказанная функция работала с несколькими вложенными функциями, вам, вероятно, понадобится что-то, что «развернет» ваши вложенные функции. Я не проверял это, но следующая функция может привести вас на правильный путь.

<?php
function unwrap($str) {
  $pos = strpos($str, "(");
  if ($pos === false) return $str; // There's no function call here

  $last_close = 0;
  $cur_offset = 0; // Start at the beginning
  while ($cur_offset <= strlen($str)) {
    $first_close = strpos($str, ")", $offset); // Find first deep function
    $pos = strrpos($str, "(", $first_close-1); // Find associated opening
    if ($pos > $last_close) {
      // This function is entirely after the previous function
      $ticked = Tick(substr($str, $pos+1, $first_close-$pos)); // Tick the string inside
      $str = substr($str, 0, $pos)."{".$ticked."}".substr($str,$first_close); // Replace parenthesis by curly braces temporarily
      $first_close += strlen($ticked)-($first_close-$pos); // Shift parenthesis location due to new ticks being added
    } else {
      // This function wraps other functions; don't tick it
      $str = substr($str, 0, $pos)."{".substr($str,$pos+1, $first_close-$pos)."}".substr($str,$first_close);
    }
    $last_close = $first_close;
    $offset = $first_close+1;
  }
  // Replace the curly braces with parenthesis again
  $str = str_replace(array("{","}"), array("(",")"), $str);
}
1 голос
/ 02 сентября 2009

Если вы добавляете вызовы функций в свой код, в отличие от передачи их через интерфейс только для строк, вы можете заменить разбор строк проверкой типа:

function Tick($value) {
    if (is_object($value)) {
        $result = $value->value;
    } else {
        $result = '`'.str_replace(array('`', '.'), array('', '`.`'), $value).'`';
    }

    return $result;
}

class SqlFunction {
    var $value;
    function SqlFunction($function, $params) {
        $sane = implode(', ', array_map('Tick', $params));
        $this->value = "$function($sane)";
    }
}

function Maximum($column) {
    return new SqlFunction('MAX', array($column));
}

function Avg($column) {
    return new SqlFunction('AVG', array($column));
}

function Greatest() {
    $params = func_get_args();
    return new SqlFunction('GREATEST', $params);
}

$cases = array(
    "'simple'" => Tick('simple'),
    "'table.field'" => Tick('table.field'),
    "'table.*'" => Tick('table.*'),
    "'evil`hack'" => Tick('evil`hack'),
    "Avg('database.table.field')" => Tick(Avg('database.table.field')),
    "Greatest(Avg('table.field1'), Maximum('table.field2'))" => Tick(Greatest(Avg('table.field1'), Maximum('table.field2'))),
);

echo "<table>";
foreach ($cases as $case => $result) {
    echo "<tr><td>$case</td><td>$result</td></tr>";
}

echo "</table>";

Это позволяет избежать любых возможных SQL-инъекций, оставаясь читабельным для будущих читателей вашего кода.

0 голосов
/ 01 сентября 2009

Вы генерируете SQL-запрос или он передается вам? Если вы сгенерируете запрос, я не передам всю строку запроса, только те пармы / значения, которые вы хотите заключить в обратные черты, или что вам еще нужно.

Пример:

   function addTick($var) {
      return '`' . $var . '`';
   }

   $condition = addTick($condition);

   $SQL = 'SELECT' . $what . ' 
      FROM ' . $table . ' 
      WHERE ' . $condition . ' = ' . $constraint;

Это просто пародия, но вы понимаете, что вы можете пропустить или пройтись по циклу в своем коде и построить строку запроса, а не анализировать строку запроса и добавлять обратные ссылки.

0 голосов
/ 26 августа 2009

Вы можете использовать preg_replace_callback() в сочетании с вашим Tick() методом, чтобы пропустить хотя бы один уровень паренов:

public function tick($str) 
{
  return preg_replace_callback('/[^()]*/', array($this, '_tick_replace_callback'), $str);
}

protected function _tick_replace_callback($str) {
  $string = explode('.', str_replace('`', '', $string));
  foreach ($string as $key => $value)
  {
    if ($value != '*')
    {
      $string[$key] = '`' . trim($value) . '`';
    }
  }
  return implode('.', $string);
}
...