📜 ⬆️ ⬇️

Automated refactoring in a big project

If you work in a large development team on the same project, then refactoring becomes a very difficult task. Here is an example: we want to rename the do_something () function to do_something_with_blackjack (). We renamed all occurrences of this function in our branch and sent the task for testing. At the same time, someone else added another function call, but with the old name, also in its branch. Individually, the changesets will work, but after the merge, you get an error.

The article will consider a technique that can be called "automated refactoring" - the use of samopisny scripts that do the necessary work for you, allowing you to refactor after merging all the branches and before directly laying on staging / production.

Using the example of phpBB, it will be shown how to “refactor” calls to SQL queries so that they use automatic screening of the input data (and thus help solve the problem of SQL injections).

Description of the approach


Let's start with the theory: we will describe the problem and the proposed ways to solve it.
')

Automatic merge change issues


Suppose we are refactoring the code and want to rename the function (in the examples - the code in PHP). Suppose we changed the code as follows (the first “-” character means deleting a line, and “+” means adding a line):

<?php -function do_something() +function print_hello() { echo "Hello world!\n"; } $a = 1; -do_something(); +print_hello(); $b = 2; $c = 3; 

While we were doing this, our colleague managed to add the use of the old function name in another branch:

  <?php function do_something() { echo "Hello world!\n"; } $a = 1; do_something(); $b = 2; +do_something(); $c = 3; 

Since the changes are consistent (we worked with different lines of code), after the automatic merging of our branches with the changes, we get the following code:

 <?php function print_hello() { echo "Hello world!\n"; } $a = 1; print_hello(); $b = 2; do_something(); $c = 3; 

Thus, in the final version there will be a call to a non-existent function (do_something). Such code obviously will not work. Some cite the possibility of such behavior as an argument against the use of “feature branches”, when one branch is used for one task. You can offer various solutions to this problem, but one thing is clear: it is not easily solved, and you have to pay a lot for the possibility of refactoring in an actively developing project.

Automated refactoring


The essence of the approach is that we have to postpone the implementation of most of the changes in the code in such a way that the replacements are performed after the merging of all branches. That is, you first need to apply the changes made by your colleagues, and only then proceed to your work and perform a renaming after all other changes. You also need to introduce a mechanism that would prohibit the use of the code in the old style.

Therefore, to perform refactoring you need to write the following scripts:


More about each of them.

Check for old code


To prevent the use of the old-style code during the refactoring process and after its completion, you need to write a script that would allow you to unequivocally and reliably establish that the code is no longer used. For our simple example, the script will look something like this:

 #!/bin/sh grep do_something file.php 

The script returns lines in the file that contain the old function name - do_something.

Perform Auto Replace


For our simple example, it will be enough to use sed to replace all the do_something entries with print_hello. In more complex cases, this will not be enough and more careful handling will be needed (for example, if the substring do_something may be present in the project and not a function call).

 #!/bin/sh sed -i 's/do_something/print_hello/g' file.php 

Collect statistics using the old and new code


Our replacement script, although it should replace all the do_something occurrences with print_hello, but it cannot do anything, for example, with this code:

 $func_prefix = "do_"; $func_name = $func_prefix . "something"; // , PHP      ,    $func_name(); 

This problem exists even in statically typed languages, such as C and Java. In C, you can always write #define and make macros, and in Java there is a Reflection mechanism. To do this, we introduce a new version of the do_something function (you need to ensure that this function is not automatically replaced by the script above):

 function do_something() { error_log(__FUNCTION__ . " found. Trace: " . (new Exception())); return print_hello(); } 

As a result, after executing unit tests or after laying out this code in production (if you do not have 100% coverage), you can find missing references to the old function. These fragments can be analyzed and handled accordingly.

Stages of application of written scripts


After the corresponding scripts are written, you need to apply them correctly. It is proposed to do this as follows:

  1. merge all the latest developer changes;
  2. run a script for automatic replacement and commit changes to the version control system;
  3. check that there are no more references to the old code;
  4. add logging using old code;
  5. run the unit tests, put the code on production;
  6. Immediately after the code hits the main development branch, you need to configure the "hooks" of your version control system so that it no longer misses the use of old code;
  7. add a check for the use of old code in scripts that run before the production code hits;
  8. (optional) collect from the production logs using the old code and fix the corresponding places manually.
  9. You have successfully refactored!

The presence of item 7 is important because the repository may contain old code in other branches that have not yet been merged with the main branch of development. The merging of such branches can take place without conflicts, but they may contain references to the old code.

An important point in the described method is the possibility of applying changes at one time, using scripts and a small amount of manual work. It is also possible to do a lot of refactoring completely manually, but this is a very difficult path to follow only if you have no other options.

Refactoring the use of SQL in phpBB


To illustrate the approach in more realistic examples, we would like to show how to refactor the phpBB code in order to save it from SQL injection as much as possible:

Work with SQL in phpBB


Let's see how the work with SQL in phpBB is arranged and why the mechanism used is imperfect.

First, the implementation of the database classes themselves is in includes / db, and the global variable $ db is created in common.php, which contains an instance of the corresponding class for working with the database.

 $ ls includes/db db_tools.php index.htm mssqlnative.php oracle.php dbal.php mssql.php mysql.php postgres.php firebird.php mssql_odbc.php mysqli.php sqlite.php 

Thus, the “includes / db” directory should be excluded from our scripts during the automatic replacement - we will perform the corresponding replacements manually.

To assess the scale of the problem, let's use a wonderful tool called grep:
 $ grep -RF 'sql_query(' * | wc -l 1611 

That is about 1,600 calls to sql_query (). We believe it becomes clear that manually replacing such a number of places is not a good idea. Probably so many places is one of the reasons why phpBB developers haven’t done anything with these calls so far.

Let's now still see what's wrong with them?

Here is the definition of sql_query:
 function sql_query($query = '', $cache_ttl = 0) … 

From here we can easily see that only the request itself receives the method, and the caller must do the escaping of the values ​​on its own.

Example from viewtopic.php:

 $sql = 'SELECT forum_id FROM ' . TOPICS_TABLE . " WHERE topic_id = $topic_id"; $result = $db->sql_query($sql); 

Since $ topic_id comes from the outside and may not be processed properly, it is possible to get both a SQL error and a real SQL injection. Using UNION can even allow us to pull data from another table, so you should avoid writing such code with all your might.

Instead, we could write something like the following:

 $sql = 'SELECT forum_id FROM ' . TOPICS_TABLE . ' WHERE topic_id = ?d'; $result = $db->sql_query_escaped($sql, $topic_id); 

That is, instead of “$ topic_id” write “? D”, which will be interpreted by the sql_query_escaped method and will always be explicitly treated as a number.

SQL queries with automatic screening of values


It is proposed for example to make the simplest wrapper of the following type and put it in dbal.php:

 /** * Execute escaped SQL query * * First parameter, $query_template is the SQL template that has the following placeholders: * * ?d - value is integer * ?s - value is string (escaped by default) * ?a - array of integers for usage like IN(?a) * ?r - raw SQL (eg for use in dynamic part of SQL expression) * * Example: * * sql_query_escaped("SELECT * FROM table WHERE table_id = ?d", 42) will be executed as * * SELECT * FROM table WHERE table_id = 42 * * sql_query_escaped("SELECT * FROM table WHERE table_id IN(?a)", array(1, 2, 3)) will be executed as * * SELECT * FROM table WHERE table_id IN(1, 2, 3) * */ function sql_query_escaped($query_template) { $values = func_get_args(); array_shift($values); $regexp = '/\\?[dsar]/s'; preg_match_all($regexp, $query_template, $matches); foreach ($matches[0] as $i => $m) { if ($m == '?d') $values[$i] = intval($values[$i]); else if ($m == '?s') $values[$i] = "'" . $this->sql_escape($values[$i]) . "'"; else if ($m == '?a') $values[$i] = implode(',', array_map('intval', $values[$i])); } $idx = 0; $replace_func = function($placeholder) use ($values, &$idx) { $placeholder = $placeholder[0]; return $values[$idx++]; }; $query = preg_replace_callback($regexp, $replace_func, $query_template); return $this->sql_query($query); } 

The implementation and the way to solve the problem are not so fundamental for us, because this is just an example. If you really want to do something like this, you can come up with a better solution or use standard solutions, such as PDO.

Debugging SQL Queries


In phpBB, for some reason, there is no easy way to output SQL queries on the current page, so we will add a small temporary patch:

 --- a/includes/db/mysqli.php +++ b/includes/db/mysqli.php @@ -151,6 +151,8 @@ class dbal_mysqli extends dbal return true; } + var $queries = array(); + /** * Base query method * @@ -162,6 +164,7 @@ class dbal_mysqli extends dbal */ function sql_query($query = '', $cache_ttl = 0) { + $this->queries[] = $query; if ($query != '') { global $cache; --- a/includes/functions.php +++ b/includes/functions.php @@ -4735,6 +4735,10 @@ function page_footer($run_cron = true) } $debug_output .= ' | <a href="' . build_url() . '&explain=1">Explain</a>'; + + foreach ($db->queries as $query) { + $debug_output .= "<pre style='text-align: left; margin-bottom: 10px;'>" . htmlspecialchars($query) . "</pre>\n<hr/>\n"; + } } } 


Analysis of the use of sql_query ()


One of the most important moments while writing scripts for automatic refactoring is the so-called “gaze method”. We need to look at how the method we are going to change is used, and find the main patterns of its use by scrutinizing the code.

So, once again we will use grep, but this time we will look through the eyes of the mention of sql_query () and try to formulate the main patterns found:
 $ grep -RF 'sql_query(' * | less 

Basic patterns:

  1. $ result = $ db-> sql_query ($ sql); // $ sql variable contains the query text;
  2. $ db-> sql_query ("DELETE | UPDATE | SELECT ..."); // request is specified explicitly.

You may also notice that the same name is always used for the database object “$ db”. However, you need to make sure that the database object is not used under a different name and there is no sql_query method in other classes:
 $ grep -RF 'sql_query(' * | grep -vF '$db->sql_query(' 

Having executed this code, we will see that there are still mentions. They can be divided into several categories:


From all this we really should pay attention to the calls to sql_query () in the heirs of dbal, as well as in the dbal itself. Thus, from our additional analysis, it became clear that we also need to refactor all the sql_query_limit calls.

Theoretically, the scripts for database conversion may also contain SQL injections. However, these scripts make up a small part of all SQL queries and most likely do not contain SQL injections because of their specificity. Therefore, these places we miss.

Pattern analysis $ db-> sql_query ('SELECT / UPDATE / DELETE ...')


Let's now write a script that will find us all the references to sql_query, when the SQL query is written directly. There are not too many such places compared to sql_query ($ sql), but first we will choose a simpler task.

The algorithm is as follows: we divide the contents of the files into tokens and find the sequence “$ db”, “->”, and “sql_query” there. After that, the expression in brackets will be the same request. The code will be written in PHP 5.3 using the tokenizer extension.

 <?php //        ,    : $files = exec('grep -RF \'$db->sql_query(\' * | awk -F: \'{print $1;}\'', $out, $retval); if ($retval) exit(1); //       replacer/ $excludes = array('includes/db/', 'replacer/'); $num_lines = 0; foreach (array_unique($out) as $filename) { foreach ($excludes as $excl) { if (strpos($filename, $excl) === 0) continue(2); } $contents = file_get_contents($filename); if ($contents === false) exit(1); $lines = explode("\n", $contents); //       $tokens = token_get_all($contents); $num = count($tokens); $line = 1; $type = $text = ''; //    5   : '$db', '->', 'sql_query', '('    //         «» $accepted_value_types = array(T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE, '"'); foreach ($tokens as $cur_idx => $tok) { parse_token($tok, $type, $text, $line); //     ,       5  if ($cur_idx >= $num - 5) continue; $ln = $line; $i = $cur_idx + 1; //      (http://php.net/manual/tokens.php) if ($type !== T_VARIABLE || $text !== '$db') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== T_OBJECT_OPERATOR || $text !== '->') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== T_STRING || $text !== 'sql_query') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== '(') continue; parse_token($tokens[$i++], $type, $text, $ln); if (!in_array($type, $accepted_value_types, true)) continue; //    :   ,      $depth = 1; $query = ''; for ($i = $i - 1; $i < $num; $i++) { parse_token($tokens[$i], $type, $text, $ln); if ($type === '(') $depth++; else if ($type === ')') $depth--; if ($depth == 0) break; $query .= $text; } $query = preg_replace(array("/[\t ]+/s", '/^/m'), array(' ', ' '), $query); $results[$filename][] = $query; $num_lines++; } } foreach ($results as $filename => $list) { echo "$filename\n"; foreach ($list as $row) echo "$row\n"; echo "\n"; } echo "Total lines recognized: $num_lines\n"; // .       token_get_all function parse_token($tok, &$type, &$text, &$line) { if (is_array($tok)) list($type, $text, $line) = $tok; else $type = $text = $tok; } 

After the launch, we will see that our script has recognized about 130 lines, which is about 8% of all the “sql_query” text entries in the project. You may also notice that a significant part of the found rows are strings of the form

 includes/acp/acp_attachments.php 'INSERT INTO ' . EXTENSIONS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary) includes/acp/acp_bbcodes.php 'INSERT INTO ' . BBCODES_TABLE . $db->sql_build_array('INSERT', $sql_ary) 

That is, the sql_build_array method is used to insert values ​​into the fields, and the values ​​in this method are already escaped and nothing can be touched here.

Analysis of the $ db-> sql_query Pattern ($ sql)


Now let's learn how to get the text of an SQL query in the case when its value is taken from the $ sql variable. Let's look at the context of the request call and try to find a way to uniquely get the request itself:
 $ grep -A 5 -B 5 -RF '$db->sql_query($sql' * | less 

From the code you can see that most of the requests are called as follows:
 $sql = '...'; //   $db->sql_query($sql); 

In other words, the definition of the $ sql variable and the assignment of a value occurs immediately before the sql_query () call. Therefore, we write the processing of just such a case as the most common and simple. First of all, we print the query strings so that it is clear what we are dealing with:

 <?php //        ,     $files = exec('grep -RF \'$db->sql_query($sql\' * | awk -F: \'{print $1;}\'', $out, $retval); if ($retval) exit(1); //       replacer/ $excludes = array('includes/db/', 'replacer/'); $num_lines = 0; foreach (array_unique($out) as $filename) { foreach ($excludes as $excl) { if (strpos($filename, $excl) === 0) continue(2); } echo "$filename\n"; $contents = file_get_contents($filename); if ($contents === false) exit(1); $lines = explode("\n", $contents); //       $tokens = token_get_all($contents); $num = count($tokens); $line = 1; $type = $text = ''; //    5   : '$db', '->', 'sql_query', '('  '$sql' foreach ($tokens as $cur_idx => $tok) { parse_token($tok, $type, $text, $line); //     ,       5  if ($cur_idx >= $num - 5) continue; $ln = $line; $i = $cur_idx + 1; //      (http://php.net/manual/tokens.php) if ($type !== T_VARIABLE || $text !== '$db') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== T_OBJECT_OPERATOR || $text !== '->') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== T_STRING || $text !== 'sql_query') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== '(') continue; parse_token($tokens[$i++], $type, $text, $ln); if ($type !== T_VARIABLE || $text !== '$sql') continue; //     .     grep printf("%6d %s\n", $line, ltrim($lines[$line - 1])); $positions = find_dollar_sql_assignment($tokens, $cur_idx); if (!is_array($positions)) { echo " $positions\n\n"; continue; } $query = ''; for ($i = $positions['begin']; $i <= $positions['end']; $i++) { parse_token($tokens[$i], $type, $text, $ln); $query .= $text; } $query = preg_replace(array("/[\t ]+/s", '/^/m'), array(' ', ' '), $query); echo "\n" . $query . "\n\n"; $num_lines++; } echo "\n"; } echo "Total lines recognized: $num_lines\n"; // .       token_get_all function parse_token($tok, &$type, &$text, &$line) { if (is_array($tok)) list($type, $text, $line) = $tok; else $type = $text = $tok; } //  $sql = '...';  $db->sql_query($sql function find_dollar_sql_assignment($tokens, $db_idx) { $depth = 0; $line = 1; $type = $text = ''; //        $sql for ($idx = $db_idx - 1; $idx > 0; $idx--) { parse_token($tokens[$idx], $type, $text, $line); //    if ($type === ')') $depth++; else if ($type === '(') $depth--; if ($depth < 0) { return "depth < 0 on line " . __LINE__; } /*         ,      : if (...) { $sql = 'SELECT * FROM Table1 WHERE user_id = $user_id'; } else { $sql = 'SELECT * FROM Table2'; } $result = $db->sql_query($sql); */ if ($type === '{' || $type === '}') { return "open/close brace found on line " . __LINE__; } if ($type === T_VARIABLE && $text === '$sql') { $i = $idx + 1; parse_token($tokens[$i++], $type, $text, $line); if ($type === T_WHITESPACE) parse_token($tokens[$i++], $type, $text, $line); if ($type !== '=') continue; //   "$sql = " ! $begin_pos = $i; break; } } //      «$sql = »,     //  ,        «;» $depth = 0; for ($idx = $begin_pos; $idx < $db_idx; $idx++) { parse_token($tokens[$idx], $type, $text, $line); if ($type === '(') $depth++; else if ($type === ')') $depth--; if ($depth < 0) { return "depth < 0 on line " . __LINE__; } if ($depth === 0 && $type === ';') { return array('begin' => $begin_pos, 'end' => $idx - 1); } } //   ,       $db->sql_query  «;» //  ,     return "Statement does not terminate on line " . __LINE__; } 

In addition to the direct output of queries, we also display at the end the number of recognized lines (about 1150). This represents approximately 70% of all sql_query text entries! Let's now give examples of the output of our script:

 cron.php 59 $db->sql_query($sql); 'UPDATE ' . CONFIG_TABLE . " SET config_value = '" . $db->sql_escape(CRON_ID) . "' WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape($config['cron_lock']) . "'" … includes/acp/acp_forums.php 243 $result = $db->sql_query($sql); 'SELECT * FROM ' . FORUMS_TABLE . " WHERE forum_id = $forum_id" ... 1096 $db->sql_query($sql); 'DELETE FROM ' . FORUMS_TABLE . ' WHERE ' . $db->sql_in_set('forum_id', $forum_ids) includes/acp/acp_reasons.php ... 96 $result = $db->sql_query($sql); 'SELECT reason_id FROM ' . REPORTS_REASONS_TABLE . " WHERE reason_title = '" . $db->sql_escape($reason_row['reason_title']) . "'" ... includes/acp/acp_search.php 320 $result = $db->sql_query($sql); 'SELECT post_id, poster_id, forum_id FROM ' . POSTS_TABLE . ' WHERE post_id >= ' . (int) ($post_counter + 1) . ' AND post_id <= ' . (int) ($post_counter + $this->batch_size) 

Pay attention to the direct use of the $ db-> sql_escape and $ db-> sql_in_set methods: we should handle calls of such methods separately and take into account the presence of screening in the direct code. Also calls to intval () and type conversion to int should also be considered when rewriting this code.

Request text conversion


Now that we have learned how to isolate the query text, it's time to write a function with the ability to accept an array of tokens that match the query, and at the output return another array of tokens containing the query template for sql_query_escaped, as well as an array of arguments that will be passed to this method. In other words, we want the following:

 $sql = /*    */ 'SELECT user_id, author_id FROM ' . PRIVMSGS_TO_TABLE . ' WHERE msg_id = ' . $attachment['post_msg_id'] /*   */; $result = $db->sql_query($sql); //     $sql = 'SELECT user_id, author_id FROM ' . PRIVMSGS_TO_TABLE . ' WHERE msg_id = ?d'; $result = $db->sql_query_escaped($sql, $attachment['post_msg_id']); 

«?d», , , «_id», . , , «» .

, , . , , . '?r', sql_query() , sql_query_escaped(), .

 function rewrite_tokens($in_tokens) { static $string_types = array(T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE, '"'); $type = $text = null; $line = 1; //   ,     ,  ,  ,  : // ($action == 'add') ? 'INSERT INTO ' . EXTENSION_GROUPS_TABLE . ' ' : 'UPDATE ' . EXTENSION_GROUPS_TABLE . ' SET ' parse_token($in_tokens[0], $type, $text, $line); if ($type === T_WHITESPACE) parse_token($in_tokens[1], $type, $text, $line); if (!in_array($type, $string_types, true)) return "Expected first token to be string (got $type)"; $out_params = $out_tokens = array(); //    ,     : // // 'SELECT * FROM ' . TABLE_SOMETHING . ' WHERE lang_id = ' . intval($lang_id) // //    // // array(array('SELECT * FROM ', TABLE_SOMETHING), array(' WHERE lang_id = ', intval($lang_id))) // // ,       ,      $state = 'begin'; $left_tokens = $right_tokens = $groups = array(); $depth = 0; //   foreach ($in_tokens as $tok) { parse_token($tok, $type, $text, $line); if ($type === '(') $depth++; else if ($type === ')') $depth--; if ($depth < 0) return "Brace depth less than zero"; if ($state == 'begin') { //    «» ,      if ($depth > 0 || $type !== '.') { $left_tokens[] = $tok; } else { $state = 'end'; } } else { //   ,          $groups if ($depth > 0 || $type !== '.') { $right_tokens[] = $tok; } else { $state = 'begin'; $groups[] = array($left_tokens, $right_tokens); $left_tokens = $right_tokens = array(); } } } //   -  ,     if (count($left_tokens)) $groups[] = array($left_tokens, $right_tokens); foreach ($groups as $grp) { list($left_tokens, $right_tokens) = $grp; //    ,        parse_token($left_tokens[0], $type, $text, $line); if ($type === T_WHITESPACE) parse_token($left_tokens[1], $type, $text, $line); if (!in_array($type, $string_types, true)) return "first token in a group is not string"; //   ,     foreach ($left_tokens as $tok) { parse_token($left_tokens[0], $type, $text, $line); $out_tokens[] = $tok; } if (!count($right_tokens)) break; $out_tokens[] = '.'; //   -    *_TABLE,  ,   $param = tokens_to_string($right_tokens); if (preg_match('/^\\s*([A-Z0-9_]+)_TABLE\\s*$/s', $param)) { foreach ($right_tokens as $tok) $out_tokens[] = $tok; } else { //    '?r', «»  $out_tokens[] = array(T_CONSTANT_ENCAPSED_STRING, "'?r'", $line); $out_params[] = $param; } $out_tokens[] = '.'; } //       , //  ,    syntax error parse_token(end($out_tokens), $type, $text, $line); if ($type === '.') array_pop($out_tokens); return array( 'tokens' => $out_tokens, 'params' => $out_params, ); } function tokens_to_string($tokens) { $str = ''; $type = $text = null; $line = 1; foreach ($tokens as $tok) { parse_token($tok, $type, $text, $line); $str .= $text; } $str = preg_replace(array("/[\t ]+/s", '/^/m'), array(' ', ' '), $str); return $str; } 

, ( cron.php):

 $sql = 'UPDATE ' . CONFIG_TABLE . " SET config_value = '" .'?r'. "' WHERE config_name = 'cron_lock' AND config_value = '" .'?r'. "'"; $db->sql_query_escaped($sql, $db->sql_escape(CRON_ID), $db->sql_escape($config['cron_lock'])); 

:

  $sql = 'UPDATE ' . CONFIG_TABLE . " - SET config_value = '" . $db->sql_escape(CRON_ID) . "' - WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape($config['cron_lock']) . "'"; -$db->sql_query($sql); + SET config_value = '" .'?r'. "' + WHERE config_name = 'cron_lock' AND config_value = '" .'?r'. "'"; +$db->sql_query_escaped($sql, $db->sql_escape(CRON_ID), $db->sql_escape($config['cron_lock'])); 

, $sql:

 - $db->sql_query('UPDATE ' . CONFIG_TABLE . ' SET config_value = ' . $sql_update . " WHERE config_name = '" . $db->sql_escape($config_name) . "'"); + $db->sql_query_escaped('UPDATE ' . CONFIG_TABLE . ' SET config_value = ' .'?r'. " WHERE config_name = '" .'?r'. "'", $sql_update, $db->sql_escape($config_name)); 

- '?r', . ( «WHERE id = $forum_id»). , «» . , .

, :
 $ grep -RF 'sql_query(' * | wc -l 335 

, 80% . 20% , , 80% , .

,


, «» . , , :


"?r", :



replacer/direct_sql.php — SQL sql_query()
replacer/sql_assignment.php — sql_query($sql)
replacer/rewrite.php — SQL

Source: https://habr.com/ru/post/167337/


All Articles