Linter.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. /**
  3. * Analyzes a query and gives user feedback.
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\SqlParser\Lexer;
  8. use PhpMyAdmin\SqlParser\Parser;
  9. use PhpMyAdmin\SqlParser\UtfString;
  10. use PhpMyAdmin\SqlParser\Utils\Error as ParserError;
  11. use function defined;
  12. use function htmlspecialchars;
  13. use function mb_strlen;
  14. use function sprintf;
  15. use function strlen;
  16. /**
  17. * The linter itself.
  18. */
  19. class Linter
  20. {
  21. /**
  22. * Gets the starting position of each line.
  23. *
  24. * @param string $str String to be analyzed.
  25. *
  26. * @return array
  27. */
  28. public static function getLines($str)
  29. {
  30. if ((! ($str instanceof UtfString))
  31. && defined('USE_UTF_STRINGS')
  32. && USE_UTF_STRINGS
  33. ) {
  34. // If the lexer uses UtfString for processing then the position will
  35. // represent the position of the character and not the position of
  36. // the byte.
  37. $str = new UtfString($str);
  38. }
  39. // The reason for using the strlen is that the length
  40. // required is the length in bytes, not characters.
  41. //
  42. // Given the following string: `????+`, where `?` represents a
  43. // multi-byte character (lets assume that every `?` is a 2-byte
  44. // character) and `+` is a newline, the first value of `$i` is `0`
  45. // and the last one is `4` (because there are 5 characters). Bytes
  46. // `$str[0]` and `$str[1]` are the first character, `$str[2]` and
  47. // `$str[3]` are the second one and `$str[4]` is going to be the
  48. // first byte of the third character. The fourth and the last one
  49. // (which is actually a new line) aren't going to be processed at
  50. // all.
  51. $len = $str instanceof UtfString ?
  52. $str->length() : strlen($str);
  53. $lines = [0];
  54. for ($i = 0; $i < $len; ++$i) {
  55. if ($str[$i] !== "\n") {
  56. continue;
  57. }
  58. $lines[] = $i + 1;
  59. }
  60. return $lines;
  61. }
  62. /**
  63. * Computes the number of the line and column given an absolute position.
  64. *
  65. * @param array $lines The starting position of each line.
  66. * @param int $pos The absolute position
  67. *
  68. * @return array
  69. */
  70. public static function findLineNumberAndColumn(array $lines, $pos)
  71. {
  72. $line = 0;
  73. foreach ($lines as $lineNo => $lineStart) {
  74. if ($lineStart > $pos) {
  75. break;
  76. }
  77. $line = $lineNo;
  78. }
  79. return [
  80. $line,
  81. $pos - $lines[$line],
  82. ];
  83. }
  84. /**
  85. * Runs the linting process.
  86. *
  87. * @param string $query The query to be checked.
  88. *
  89. * @return array
  90. */
  91. public static function lint($query)
  92. {
  93. // Disabling lint for huge queries to save some resources.
  94. if (mb_strlen($query) > 10000) {
  95. return [
  96. [
  97. 'message' => __(
  98. 'Linting is disabled for this query because it exceeds the '
  99. . 'maximum length.'
  100. ),
  101. 'fromLine' => 0,
  102. 'fromColumn' => 0,
  103. 'toLine' => 0,
  104. 'toColumn' => 0,
  105. 'severity' => 'warning',
  106. ],
  107. ];
  108. }
  109. /**
  110. * Lexer used for tokenizing the query.
  111. *
  112. * @var Lexer
  113. */
  114. $lexer = new Lexer($query);
  115. /**
  116. * Parsed used for analysing the query.
  117. *
  118. * @var Parser
  119. */
  120. $parser = new Parser($lexer->list);
  121. /**
  122. * Array containing all errors.
  123. *
  124. * @var array
  125. */
  126. $errors = ParserError::get([$lexer, $parser]);
  127. /**
  128. * The response containing of all errors.
  129. *
  130. * @var array
  131. */
  132. $response = [];
  133. /**
  134. * The starting position for each line.
  135. *
  136. * CodeMirror requires relative position to line, but the parser stores
  137. * only the absolute position of the character in string.
  138. *
  139. * @var array
  140. */
  141. $lines = static::getLines($query);
  142. // Building the response.
  143. foreach ($errors as $idx => $error) {
  144. // Starting position of the string that caused the error.
  145. [$fromLine, $fromColumn] = static::findLineNumberAndColumn(
  146. $lines,
  147. $error[3]
  148. );
  149. // Ending position of the string that caused the error.
  150. [$toLine, $toColumn] = static::findLineNumberAndColumn(
  151. $lines,
  152. $error[3] + mb_strlen((string) $error[2])
  153. );
  154. // Building the response.
  155. $response[] = [
  156. 'message' => sprintf(
  157. __('%1$s (near <code>%2$s</code>)'),
  158. htmlspecialchars((string) $error[0]),
  159. htmlspecialchars((string) $error[2])
  160. ),
  161. 'fromLine' => $fromLine,
  162. 'fromColumn' => $fromColumn,
  163. 'toLine' => $toLine,
  164. 'toColumn' => $toColumn,
  165. 'severity' => 'error',
  166. ];
  167. }
  168. // Sending back the answer.
  169. return $response;
  170. }
  171. }