commit
e4d4349024
77
js/codemirror/addon/lint/lint.css
vendored
Normal file
77
js/codemirror/addon/lint/lint.css
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
/* The lint marker gutter */
|
||||
.CodeMirror-lint-markers {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: infobackground;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
color: infotext;
|
||||
font-size: 10pt;
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
position: fixed;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
-moz-transition: opacity .4s;
|
||||
-webkit-transition: opacity .4s;
|
||||
-o-transition: opacity .4s;
|
||||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip code {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image:
|
||||
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
|
||||
;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
209
js/codemirror/addon/lint/lint.js
vendored
Normal file
209
js/codemirror/addon/lint/lint.js
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||
|
||||
function showTooltip(e, content) {
|
||||
var tt = document.createElement("div");
|
||||
tt.className = "CodeMirror-lint-tooltip";
|
||||
tt.appendChild(content.cloneNode(true));
|
||||
document.body.appendChild(tt);
|
||||
|
||||
function position(e) {
|
||||
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
|
||||
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
|
||||
tt.style.left = (e.clientX + 5) + "px";
|
||||
}
|
||||
CodeMirror.on(document, "mousemove", position);
|
||||
position(e);
|
||||
if (tt.style.opacity != null) tt.style.opacity = 1;
|
||||
return tt;
|
||||
}
|
||||
function rm(elt) {
|
||||
if (elt.parentNode) elt.parentNode.removeChild(elt);
|
||||
}
|
||||
function hideTooltip(tt) {
|
||||
if (!tt.parentNode) return;
|
||||
if (tt.style.opacity == null) rm(tt);
|
||||
tt.style.opacity = 0;
|
||||
setTimeout(function() { rm(tt); }, 600);
|
||||
}
|
||||
|
||||
function showTooltipFor(e, content, node) {
|
||||
var tooltip = showTooltip(e, content);
|
||||
function hide() {
|
||||
CodeMirror.off(node, "mouseout", hide);
|
||||
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||
}
|
||||
var poll = setInterval(function() {
|
||||
if (tooltip) for (var n = node;; n = n.parentNode) {
|
||||
if (n && n.nodeType == 11) n = n.host;
|
||||
if (n == document.body) return;
|
||||
if (!n) { hide(); break; }
|
||||
}
|
||||
if (!tooltip) return clearInterval(poll);
|
||||
}, 400);
|
||||
CodeMirror.on(node, "mouseout", hide);
|
||||
}
|
||||
|
||||
function LintState(cm, options, hasGutter) {
|
||||
this.marked = [];
|
||||
this.options = options;
|
||||
this.timeout = null;
|
||||
this.hasGutter = hasGutter;
|
||||
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||
}
|
||||
|
||||
function parseOptions(_cm, options) {
|
||||
if (options instanceof Function) return {getAnnotations: options};
|
||||
if (!options || options === true) options = {};
|
||||
return options;
|
||||
}
|
||||
|
||||
function clearMarks(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||
for (var i = 0; i < state.marked.length; ++i)
|
||||
state.marked[i].clear();
|
||||
state.marked.length = 0;
|
||||
}
|
||||
|
||||
function makeMarker(labels, severity, multiple, tooltips) {
|
||||
var marker = document.createElement("div"), inner = marker;
|
||||
marker.className = "CodeMirror-lint-marker-" + severity;
|
||||
if (multiple) {
|
||||
inner = marker.appendChild(document.createElement("div"));
|
||||
inner.className = "CodeMirror-lint-marker-multiple";
|
||||
}
|
||||
|
||||
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||
showTooltipFor(e, labels, inner);
|
||||
});
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
function getMaxSeverity(a, b) {
|
||||
if (a == "error") return a;
|
||||
else return b;
|
||||
}
|
||||
|
||||
function groupByLine(annotations) {
|
||||
var lines = [];
|
||||
for (var i = 0; i < annotations.length; ++i) {
|
||||
var ann = annotations[i], line = ann.from.line;
|
||||
(lines[line] || (lines[line] = [])).push(ann);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function annotationTooltip(ann) {
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
var tip = document.createElement("div");
|
||||
tip.className = "CodeMirror-lint-message-" + severity;
|
||||
tip.appendChild(document.createTextNode(ann.message));
|
||||
// Unescaping only the <code> tag.
|
||||
tip.innerHTML = tip.innerHTML.replace("<code>", "<code>")
|
||||
.replace("</code>", "</code>");
|
||||
return tip;
|
||||
}
|
||||
|
||||
function startLinting(cm) {
|
||||
var state = cm.state.lint, options = state.options;
|
||||
var passOptions = options.options || options; // Support deprecated passing of `options` property in options
|
||||
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||
if (!getAnnotations) return;
|
||||
if (options.async || getAnnotations.async)
|
||||
getAnnotations(cm.getValue(), updateLinting, passOptions, cm);
|
||||
else
|
||||
updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm));
|
||||
}
|
||||
|
||||
function updateLinting(cm, annotationsNotSorted) {
|
||||
clearMarks(cm);
|
||||
var state = cm.state.lint, options = state.options;
|
||||
|
||||
var annotations = groupByLine(annotationsNotSorted);
|
||||
|
||||
for (var line = 0; line < annotations.length; ++line) {
|
||||
var anns = annotations[line];
|
||||
if (!anns) continue;
|
||||
|
||||
var maxSeverity = null;
|
||||
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||
|
||||
for (var i = 0; i < anns.length; ++i) {
|
||||
var ann = anns[i];
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
maxSeverity = getMaxSeverity(maxSeverity, severity);
|
||||
|
||||
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
|
||||
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||
|
||||
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||
className: "CodeMirror-lint-mark-" + severity,
|
||||
__annotation: ann
|
||||
}));
|
||||
}
|
||||
|
||||
if (state.hasGutter)
|
||||
cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
|
||||
state.options.tooltips));
|
||||
}
|
||||
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
clearTimeout(state.timeout);
|
||||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
|
||||
}
|
||||
|
||||
function popupSpanTooltip(ann, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
showTooltipFor(e, annotationTooltip(ann), target);
|
||||
}
|
||||
|
||||
function onMouseOver(cm, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
|
||||
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
|
||||
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||
for (var i = 0; i < spans.length; ++i) {
|
||||
var ann = spans[i].__annotation;
|
||||
if (ann) return popupSpanTooltip(ann, e);
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
clearMarks(cm);
|
||||
cm.off("change", onChange);
|
||||
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
|
||||
clearTimeout(cm.state.lint.timeout);
|
||||
delete cm.state.lint;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
|
||||
cm.on("change", onChange);
|
||||
if (state.options.tooltips != false)
|
||||
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||
|
||||
startLinting(cm);
|
||||
}
|
||||
});
|
||||
});
|
||||
37
js/codemirror/addon/lint/sql-lint.js
vendored
Normal file
37
js/codemirror/addon/lint/sql-lint.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
CodeMirror.sqlLint = function(text, updateLinting, options, cm) {
|
||||
|
||||
// Skipping check if text box is empty.
|
||||
if(text.trim() == "") {
|
||||
updateLinting(cm, []);
|
||||
return;
|
||||
}
|
||||
|
||||
function handleResponse(json) {
|
||||
response = JSON.parse(json);
|
||||
|
||||
var found = [];
|
||||
for (var idx in response) {
|
||||
found.push({
|
||||
from: CodeMirror.Pos(
|
||||
response[idx].fromLine, response[idx].fromColumn
|
||||
),
|
||||
to: CodeMirror.Pos(
|
||||
response[idx].toLine, response[idx].toColumn
|
||||
),
|
||||
message: response[idx].message,
|
||||
severity : response[idx].severity
|
||||
});
|
||||
}
|
||||
|
||||
updateLinting(cm, found);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "lint.php",
|
||||
data: {
|
||||
'sql_query': text
|
||||
},
|
||||
success: handleResponse
|
||||
});
|
||||
}
|
||||
@ -112,6 +112,17 @@ function PMA_getSQLEditor($textarea, options, resize) {
|
||||
mode: "text/x-mysql",
|
||||
lineWrapping: true
|
||||
};
|
||||
|
||||
if (CodeMirror.sqlLint) {
|
||||
$.extend(defaults, {
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lint: {
|
||||
"getAnnotations": CodeMirror.sqlLint,
|
||||
"async": true,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.extend(true, defaults, options);
|
||||
|
||||
// create CodeMirror editor
|
||||
|
||||
@ -407,6 +407,10 @@ class PMA_Header
|
||||
$this->_scripts->addFile('codemirror/addon/runmode/runmode.js');
|
||||
$this->_scripts->addFile('codemirror/addon/hint/show-hint.js');
|
||||
$this->_scripts->addFile('codemirror/addon/hint/sql-hint.js');
|
||||
if ($GLOBALS['cfg']['LintEnable']) {
|
||||
$this->_scripts->addFile('codemirror/addon/lint/lint.js');
|
||||
$this->_scripts->addFile('codemirror/addon/lint/sql-lint.js');
|
||||
}
|
||||
}
|
||||
$this->_scripts->addCode(
|
||||
'ConsoleEnterExecutes='
|
||||
@ -658,6 +662,8 @@ class PMA_Header
|
||||
. $basedir . 'js/codemirror/lib/codemirror.css" />';
|
||||
$retval .= '<link rel="stylesheet" type="text/css" href="'
|
||||
. $basedir . 'js/codemirror/addon/hint/show-hint.css" />';
|
||||
$retval .= '<link rel="stylesheet" type="text/css" href="'
|
||||
. $basedir . 'js/codemirror/addon/lint/lint.css" />';
|
||||
$retval .= '<link rel="stylesheet" type="text/css" href="'
|
||||
. $basedir . 'phpmyadmin.css.php?'
|
||||
. 'nocache=' . $theme_id . $GLOBALS['text_dir'] . '" />';
|
||||
|
||||
162
libraries/Linter.class.php
Normal file
162
libraries/Linter.class.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Analyzes a query and gives user feedback.
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
if (! defined('PHPMYADMIN')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The linter itself.
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
class PMA_Linter
|
||||
{
|
||||
|
||||
/**
|
||||
* Gets the starting position of each line.
|
||||
*
|
||||
* @param string $str String to be analyzed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getLines($str)
|
||||
{
|
||||
$lines = array(0);
|
||||
|
||||
// The reason for using the '8bit' parameter is that the length
|
||||
// required is the length in bytes, not characters.
|
||||
//
|
||||
// Given the following string: `????+`, where `?` represents a
|
||||
// multi-byte character (lets assume that every `?` is a 2-byte
|
||||
// character) and `+` is a newline, the first value of `$i` is `0` and
|
||||
// the last one is `4` (because there are 5 characters). Bytes `$str[0]`
|
||||
// and `$str[1]` are the first character, `$str[2]` and `$str[3]` are
|
||||
// the second one and `$str[4]` is going to be the first byte of the
|
||||
// third character. The fourth and the last one (which is actually a new
|
||||
// line) aren't going to be processed at all.
|
||||
for ($i = 0, $len = /*overload*/mb_strlen($str, '8bit'); $i < $len; ++$i) {
|
||||
if ($str[$i] === "\n") {
|
||||
$lines[] = $i + 1;
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of the line and column given an absolute position.
|
||||
*
|
||||
* @param array $lines The starting position of each line.
|
||||
* @param int $pos The absolute position
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function findLineNumberAndColumn($lines, $pos)
|
||||
{
|
||||
$line = 0;
|
||||
foreach ($lines as $lineNo => $lineStart) {
|
||||
if ($lineStart > $pos) {
|
||||
break;
|
||||
}
|
||||
$line = $lineNo;
|
||||
}
|
||||
return array($line, $pos - $lines[$line]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the linting process.
|
||||
*
|
||||
* @param string $query The query to be checked.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function lint($query)
|
||||
{
|
||||
// Disabling lint for huge queries to save some resources.
|
||||
if (/*overload*/mb_strlen($query) > 10000) {
|
||||
echo json_encode(
|
||||
array(
|
||||
array(
|
||||
'message' => 'The linting is disabled for this query because it exceededs the maxmimum length.',
|
||||
'fromLine' => 0,
|
||||
'fromColumn' => 0,
|
||||
'toLine' => 0,
|
||||
'toColumn' => 0,
|
||||
'severity' => 'warning',
|
||||
)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexer used for tokenizing the query.
|
||||
*
|
||||
* @var SqlParser\Lexer
|
||||
*/
|
||||
$lexer = new SqlParser\Lexer($query);
|
||||
|
||||
/**
|
||||
* Parsed used for analysing the query.
|
||||
*
|
||||
* @var SqlParser\Parser
|
||||
*/
|
||||
$parser = new SqlParser\Parser($lexer->list);
|
||||
|
||||
/**
|
||||
* Array containing all errors.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
$errors = SqlParser\Utils\Error::get(array($lexer, $parser));
|
||||
|
||||
/**
|
||||
* The response containing of all errors.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
$response = array();
|
||||
|
||||
/**
|
||||
* The starting position for each line.
|
||||
*
|
||||
* CodeMirror requires relative position to line, but the parser stores
|
||||
* only the absolute position of the character in string.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
$lines = static::getLines($query);
|
||||
|
||||
// Building the response.
|
||||
foreach ($errors as $idx => $error) {
|
||||
|
||||
// Starting position of the string that caused the error.
|
||||
list($fromLine, $fromColumn) = static::findLineNumberAndColumn(
|
||||
$lines, $error[3]
|
||||
);
|
||||
|
||||
// Ending position of the string that caused the error.
|
||||
list($toLine, $toColumn) = static::findLineNumberAndColumn(
|
||||
$lines, $error[3] + /*overload*/mb_strlen($error[2])
|
||||
);
|
||||
|
||||
// Building the response.
|
||||
$response[] = array(
|
||||
'message' => $error[0] . ' (near <code>' . $error[2] . '</code>)',
|
||||
'fromLine' => $fromLine,
|
||||
'fromColumn' => $fromColumn,
|
||||
'toLine' => $toLine,
|
||||
'toColumn' => $toColumn,
|
||||
'severity' => 'error',
|
||||
);
|
||||
}
|
||||
|
||||
// Sending back the answer.
|
||||
echo json_encode($response);
|
||||
}
|
||||
|
||||
}
|
||||
@ -757,6 +757,13 @@ $cfg['RetainQueryBox'] = false;
|
||||
*/
|
||||
$cfg['CodemirrorEnable'] = true;
|
||||
|
||||
/**
|
||||
* use the parser to find any errors in the query before executing
|
||||
*
|
||||
* @global boolean $cfg['LintEnable']
|
||||
*/
|
||||
$cfg['LintEnable'] = true;
|
||||
|
||||
/**
|
||||
* show a 'Drop database' link to normal users
|
||||
*
|
||||
|
||||
@ -57,6 +57,9 @@ $strConfigCodemirrorEnable_desc = __(
|
||||
. 'line numbers.'
|
||||
);
|
||||
$strConfigCodemirrorEnable_name = __('Enable CodeMirror');
|
||||
$strConfigLintEnable_desc = __('Find any erorors in the query before executing it.'
|
||||
. 'Requires CodeMirror to be enabled.');
|
||||
$strConfigLintEnable_name = __('Enable linter');
|
||||
$strConfigMinSizeForInputField_desc = __(
|
||||
'Defines the minimum size for input fields generated for CHAR and VARCHAR '
|
||||
. 'columns.'
|
||||
|
||||
@ -162,6 +162,7 @@ $forms['Sql_queries']['Sql_queries'] = array(
|
||||
'MaxCharactersInDisplayedSQL',
|
||||
'RetainQueryBox',
|
||||
'CodemirrorEnable',
|
||||
'LintEnable',
|
||||
'EnableAutocompleteForTablesAndColumns',
|
||||
'DefaultForeignKeyChecks');
|
||||
$forms['Sql_queries']['Sql_box'] = array('SQLQuery' => array(
|
||||
|
||||
@ -70,6 +70,7 @@ $forms['Sql_queries']['Sql_queries'] = array(
|
||||
'MaxCharactersInDisplayedSQL',
|
||||
'RetainQueryBox',
|
||||
'CodemirrorEnable',
|
||||
'LintEnable',
|
||||
'EnableAutocompleteForTablesAndColumns',
|
||||
'DefaultForeignKeyChecks');
|
||||
$forms['Sql_queries']['Sql_box'] = array(
|
||||
|
||||
@ -139,12 +139,19 @@ class AlterOperation extends Component
|
||||
break;
|
||||
}
|
||||
|
||||
// Skipping whitespaces and comments.
|
||||
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
|
||||
if ($state !== 2) {
|
||||
// State 2 parses the unknown part which must include whitespaces as well.
|
||||
continue;
|
||||
// Skipping comments.
|
||||
if ($token->type === Token::TYPE_COMMENT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skipping whitespaces.
|
||||
if ($token->type === Token::TYPE_WHITESPACE) {
|
||||
if ($state === 2) {
|
||||
// When parsing the unknown part, the whitespaces are
|
||||
// included to not break anything.
|
||||
$ret->unknown[] = $token;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($state === 0) {
|
||||
@ -179,6 +186,11 @@ class AlterOperation extends Component
|
||||
}
|
||||
}
|
||||
|
||||
if ($ret->options->isEmpty()) {
|
||||
$parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
|
||||
return null;
|
||||
}
|
||||
|
||||
--$list->idx;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
@ -25,40 +25,39 @@ use SqlParser\TokensList;
|
||||
class Array2d extends Component
|
||||
{
|
||||
|
||||
/**
|
||||
* An array with the values of the row to be inserted.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $values;
|
||||
|
||||
/**
|
||||
* @param Parser $parser The parser that serves as context.
|
||||
* @param TokensList $list The list of tokens that are being parsed.
|
||||
* @param array $options Parameters for parsing.
|
||||
*
|
||||
* @return Array2d
|
||||
* @return ArrayObj[]
|
||||
*/
|
||||
public static function parse(Parser $parser, TokensList $list, array $options = array())
|
||||
{
|
||||
$ret = array();
|
||||
|
||||
$expr = new Array2d();
|
||||
$value = '';
|
||||
/**
|
||||
* Whether an array was parsed or not. To be a valid parsing, at least
|
||||
* one array must be parsed after each comma.
|
||||
* @var bool $parsed
|
||||
*/
|
||||
$parsed = false;
|
||||
|
||||
/**
|
||||
* The number of values in each set.
|
||||
* @var int
|
||||
*/
|
||||
$count = -1;
|
||||
|
||||
/**
|
||||
* The state of the parser.
|
||||
*
|
||||
* Below are the states of the parser.
|
||||
*
|
||||
* 0 ------------------------[ ( ]-----------------------> 1
|
||||
* 0 ----------------------[ array ]---------------------> 1
|
||||
*
|
||||
* 1 ----------------------[ value ]---------------------> 2
|
||||
*
|
||||
* 2 ------------------------[ , ]-----------------------> 1
|
||||
* 2 ------------------------[ ) ]-----------------------> 3
|
||||
*
|
||||
* 3 ---------------------[ options ]--------------------> 4
|
||||
* 1 ------------------------[ , ]------------------------> 0
|
||||
* 1 -----------------------[ else ]----------------------> -1
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
@ -86,40 +85,36 @@ class Array2d extends Component
|
||||
break;
|
||||
}
|
||||
|
||||
if ($token->type === Token::TYPE_OPERATOR) {
|
||||
if ($state === 0) {
|
||||
if ($token->value === '(') {
|
||||
$state = 1;
|
||||
continue;
|
||||
} elseif ($token->value === ',') {
|
||||
if ($state !== 3) {
|
||||
$expr->values[] = $value;
|
||||
$value = '';
|
||||
$state = 1;
|
||||
$arr = ArrayObj::parse($parser, $list, $options);
|
||||
$arrCount = count($arr->values);
|
||||
if ($count === -1) {
|
||||
$count = $arrCount;
|
||||
} elseif ($arrCount != $count) {
|
||||
$parser->error("{$count} values were expected, but found {$arrCount}.", $token);
|
||||
}
|
||||
continue;
|
||||
} elseif ($token->value === ')') {
|
||||
$state = 3;
|
||||
$expr->values[] = $value;
|
||||
$ret[] = $expr;
|
||||
$value = '';
|
||||
$expr = new Array2d();
|
||||
continue;
|
||||
$ret[] = $arr;
|
||||
$parsed = true;
|
||||
$state = 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} elseif ($state === 1) {
|
||||
if ($token->value === ',') {
|
||||
$parsed = false;
|
||||
$state = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// No other operator is expected.
|
||||
break;
|
||||
}
|
||||
|
||||
if ($state === 1) {
|
||||
$value .= $token->value;
|
||||
$state = 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Last iteration was not saved.
|
||||
if (!empty($expr->values)) {
|
||||
$ret[] = $expr;
|
||||
if (!$parsed) {
|
||||
$parser->error(
|
||||
'An opening bracket followed by a set of values was expected.',
|
||||
$list->tokens[$list->idx]
|
||||
);
|
||||
}
|
||||
|
||||
--$list->idx;
|
||||
|
||||
@ -97,7 +97,7 @@ class ArrayObj extends Component
|
||||
|
||||
if ($state === 0) {
|
||||
if (($token->type !== Token::TYPE_OPERATOR) || ($token->value !== '(')) {
|
||||
$parser->error('An open bracket was expected.', $token);
|
||||
$parser->error('An opening bracket was expected.', $token);
|
||||
break;
|
||||
}
|
||||
$state = 1;
|
||||
@ -111,7 +111,7 @@ class ArrayObj extends Component
|
||||
$state = 2;
|
||||
} elseif ($state === 2) {
|
||||
if (($token->type !== Token::TYPE_OPERATOR) || (($token->value !== ',') && ($token->value !== ')'))) {
|
||||
$parser->error('Symbols \')\' or \',\' were expected', $token);
|
||||
$parser->error('A comma or a closing bracket was expected', $token);
|
||||
break;
|
||||
}
|
||||
if ($token->value === ',') {
|
||||
|
||||
@ -126,9 +126,9 @@ class Expression extends Component
|
||||
|
||||
/**
|
||||
* Whether a period was previously found.
|
||||
* @var bool $period
|
||||
* @var bool $dot
|
||||
*/
|
||||
$period = false;
|
||||
$dot = false;
|
||||
|
||||
/**
|
||||
* Whether an alias is expected. Is 2 if `AS` keyword was found.
|
||||
@ -146,7 +146,7 @@ class Expression extends Component
|
||||
* Keeps track of the previous token.
|
||||
* Possible values:
|
||||
* string, if function was previously found;
|
||||
* true, if open bracket was previously found;
|
||||
* true, if opening bracket was previously found;
|
||||
* null, in any other case.
|
||||
* @var string|bool $prev
|
||||
*/
|
||||
@ -169,7 +169,7 @@ class Expression extends Component
|
||||
if (($isExpr) && (!$alias)) {
|
||||
$ret->expr .= $token->token;
|
||||
}
|
||||
if (($alias === 0) && (empty($options['noAlias'])) && (!$isExpr) && (!$period) && (!empty($ret->expr))) {
|
||||
if (($alias === 0) && (empty($options['noAlias'])) && (!$isExpr) && (!$dot) && (!empty($ret->expr))) {
|
||||
$alias = 1;
|
||||
}
|
||||
continue;
|
||||
@ -202,9 +202,6 @@ class Expression extends Component
|
||||
}
|
||||
if ($token->value === '(') {
|
||||
++$brackets;
|
||||
// We don't check to see if `$prev` is `true` (open bracket
|
||||
// was found before) because the brackets count is one (the
|
||||
// only bracket we found is this one).
|
||||
if ((empty($ret->function)) && ($prev !== null) && ($prev !== true)) {
|
||||
// A function name was previously found and now an open
|
||||
// bracket, so this is a function call.
|
||||
@ -222,7 +219,7 @@ class Expression extends Component
|
||||
break;
|
||||
}
|
||||
} elseif ($brackets < 0) {
|
||||
$parser->error('Unexpected bracket.', $token);
|
||||
$parser->error('Unexpected closing bracket.', $token);
|
||||
$brackets = 0;
|
||||
}
|
||||
} elseif ($token->value === ',') {
|
||||
@ -250,28 +247,36 @@ class Expression extends Component
|
||||
// Found a `.` which means we expect a column name and
|
||||
// the column name we parsed is actually the table name
|
||||
// and the table name is actually a database name.
|
||||
if ((!empty($ret->database)) || ($period)) {
|
||||
if ((!empty($ret->database)) || ($dot)) {
|
||||
$parser->error('Unexpected dot.', $token);
|
||||
}
|
||||
$ret->database = $ret->table;
|
||||
$ret->table = $ret->column;
|
||||
$ret->column = null;
|
||||
$period = true;
|
||||
$dot = true;
|
||||
} else {
|
||||
// We found the name of a column (or table if column
|
||||
// field should be skipped; used to parse table names).
|
||||
if (!empty($options['skipColumn'])) {
|
||||
if (!empty($ret->table)) {
|
||||
$field = (!empty($options['skipColumn'])) ? 'table' : 'column';
|
||||
if (!empty($ret->$field)) {
|
||||
|
||||
// No alias is expected.
|
||||
if (!empty($options['noAlias'])) {
|
||||
break;
|
||||
}
|
||||
$ret->table = $token->value;
|
||||
|
||||
// Parsing aliases without `AS` keyword and any whitespace.
|
||||
// Example: SELECT 1`foo`
|
||||
if (($token->type === Token::TYPE_STRING)
|
||||
|| (($token->type === Token::TYPE_SYMBOL)
|
||||
&& ($token->flags & Token::FLAG_SYMBOL_BACKTICK))
|
||||
) {
|
||||
$ret->alias = $token->value;
|
||||
}
|
||||
} else {
|
||||
if (!empty($ret->column)) {
|
||||
break;
|
||||
}
|
||||
$ret->column = $token->value;
|
||||
$ret->$field = $token->value;
|
||||
}
|
||||
$period = false;
|
||||
$dot = false;
|
||||
}
|
||||
} else {
|
||||
// Parsing aliases without `AS` keyword.
|
||||
@ -298,9 +303,8 @@ class Expression extends Component
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($alias === 2) {
|
||||
$parser->error('Alias was expected.');
|
||||
$parser->error('An alias was expected.', $list->tokens[$list->idx - 1]);
|
||||
}
|
||||
|
||||
// Whitespaces might be added at the end.
|
||||
|
||||
@ -36,6 +36,11 @@ class FieldDefinition extends Component
|
||||
* @var array
|
||||
*/
|
||||
public static $FIELD_OPTIONS = array(
|
||||
|
||||
// Tells the `OptionsArray` to not sort the options.
|
||||
// See the note below.
|
||||
'_UNSORTED' => true,
|
||||
|
||||
'NOT NULL' => 1,
|
||||
'NULL' => 1,
|
||||
'DEFAULT' => array(2, 'var'),
|
||||
@ -48,13 +53,26 @@ class FieldDefinition extends Component
|
||||
'COLUMN_FORMAT' => array(6, 'var'),
|
||||
'ON UPDATE' => array(7, 'var'),
|
||||
|
||||
// MariaDB options.
|
||||
'GENERATED ALWAYS' => 1,
|
||||
'AS' => array(2, 'expr', array('bracketsDelimited' => true)),
|
||||
'VIRTUAL' => 3,
|
||||
'PERSISTENT' => 3,
|
||||
// 'UNIQUE' => 4, // common
|
||||
// 'UNIQUE KEY' => 4, // common
|
||||
// Generated columns options.
|
||||
'GENERATED ALWAYS' => 8,
|
||||
'AS' => array(9, 'expr', array('bracketsDelimited' => true)),
|
||||
'VIRTUAL' => 10,
|
||||
'PERSISTENT' => 11,
|
||||
'STORED' => 11,
|
||||
// Common entries.
|
||||
//
|
||||
// NOTE: Some of the common options are not in the same order which
|
||||
// causes troubles when checking if the options are in the right order.
|
||||
// I should find a way to define multiple sets of options and make the
|
||||
// parser select the right set.
|
||||
//
|
||||
// 'UNIQUE' => 4,
|
||||
// 'UNIQUE KEY' => 4,
|
||||
// 'COMMENT' => array(5, 'var'),
|
||||
// 'NOT NULL' => 1,
|
||||
// 'NULL' => 1,
|
||||
// 'PRIMARY' => 4,
|
||||
// 'PRIMARY KEY' => 4,
|
||||
);
|
||||
|
||||
/**
|
||||
@ -153,7 +171,7 @@ class FieldDefinition extends Component
|
||||
* 4 --------------------[ REFERENCES ]------------------> 4
|
||||
*
|
||||
* 5 ------------------------[ , ]-----------------------> 1
|
||||
* 5 ------------------------[ ) ]-----------------------> -1
|
||||
* 5 ------------------------[ ) ]-----------------------> 6 (-1)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
@ -179,6 +197,9 @@ class FieldDefinition extends Component
|
||||
if ($state === 0) {
|
||||
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
|
||||
$state = 1;
|
||||
} else {
|
||||
$parser->error('An opening bracket was expected.', $token);
|
||||
break;
|
||||
}
|
||||
} elseif ($state === 1) {
|
||||
if (($token->type === Token::TYPE_KEYWORD) && ($token->value === 'CONSTRAINT')) {
|
||||
@ -213,13 +234,12 @@ class FieldDefinition extends Component
|
||||
$expr = new FieldDefinition();
|
||||
if ($token->value === ',') {
|
||||
$state = 1;
|
||||
continue;
|
||||
} elseif ($token->value === ')') {
|
||||
$state = 6;
|
||||
++$list->idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Last iteration was not saved.
|
||||
@ -227,12 +247,16 @@ class FieldDefinition extends Component
|
||||
$ret[] = $expr;
|
||||
}
|
||||
|
||||
if (($state !== 0) && ($state !== 6)) {
|
||||
$parser->error('A closing bracket was expected.', $list->tokens[$list->idx - 1]);
|
||||
}
|
||||
|
||||
--$list->idx;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldDefinition[] $component The component to be built.
|
||||
* @param FieldDefinition|FieldDefinition[] $component The component to be built.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@ -87,7 +87,7 @@ class Limit extends Component
|
||||
|
||||
if (($token->type === Token::TYPE_KEYWORD) && ($token->value === 'OFFSET')) {
|
||||
if ($offset) {
|
||||
$parser->error('An offset was expected.');
|
||||
$parser->error('An offset was expected.', $token);
|
||||
}
|
||||
$offset = true;
|
||||
continue;
|
||||
@ -108,7 +108,7 @@ class Limit extends Component
|
||||
}
|
||||
|
||||
if ($offset) {
|
||||
$parser->error('An offset was expected.');
|
||||
$parser->error('An offset was expected.', $list->tokens[$list->idx - 1]);
|
||||
}
|
||||
|
||||
--$list->idx;
|
||||
|
||||
@ -36,7 +36,8 @@ class OptionsArray extends Component
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $options The array of options. Options that have a value
|
||||
* must be an array with two keys 'name' and 'value'.
|
||||
* must be an array with at least two keys `name` and
|
||||
* `expr` or `value`.
|
||||
*/
|
||||
public function __construct(array $options = array())
|
||||
{
|
||||
@ -162,11 +163,11 @@ class OptionsArray extends Component
|
||||
'name' => $token->value,
|
||||
// @var bool Whether it contains an equal sign.
|
||||
// This is used by the builder to rebuild it.
|
||||
'equal' => $lastOption[1] === 'var=',
|
||||
'equals' => $lastOption[1] === 'var=',
|
||||
// @var string Raw value.
|
||||
'value' => '',
|
||||
'expr' => '',
|
||||
// @var string Processed value.
|
||||
'value_' => '',
|
||||
'value' => '',
|
||||
);
|
||||
$state = 1;
|
||||
} elseif ($lastOption[1] === 'expr') {
|
||||
@ -179,14 +180,14 @@ class OptionsArray extends Component
|
||||
// @var string The name of the option.
|
||||
'name' => $token->value,
|
||||
// @var Expression The parsed expression.
|
||||
'value' => null,
|
||||
'expr' => null,
|
||||
);
|
||||
$state = 1;
|
||||
}
|
||||
} elseif ($state === 1) {
|
||||
$state = 2;
|
||||
if ($token->value === '=') {
|
||||
$ret->options[$lastOptionId]['equal'] = true;
|
||||
$ret->options[$lastOptionId]['equals'] = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -195,11 +196,13 @@ class OptionsArray extends Component
|
||||
// change this iteration.
|
||||
if ($state === 2) {
|
||||
if ($lastOption[1] === 'expr') {
|
||||
$ret->options[$lastOptionId]['value'] = Expression::parse(
|
||||
$ret->options[$lastOptionId]['expr'] = Expression::parse(
|
||||
$parser,
|
||||
$list,
|
||||
empty($lastOption[2]) ? array() : $lastOption[2]
|
||||
);
|
||||
$ret->options[$lastOptionId]['value']
|
||||
= $ret->options[$lastOptionId]['expr']->expr;
|
||||
$lastOption = null;
|
||||
$state = 0;
|
||||
} else {
|
||||
@ -209,11 +212,14 @@ class OptionsArray extends Component
|
||||
--$brackets;
|
||||
}
|
||||
|
||||
// Raw value.
|
||||
$ret->options[$lastOptionId]['value'] .= $token->token;
|
||||
$ret->options[$lastOptionId]['expr'] .= $token->token;
|
||||
|
||||
// Processed value.
|
||||
$ret->options[$lastOptionId]['value_'] .= $token->value;
|
||||
if (!((($token->token === '(') && ($brackets === 1))
|
||||
|| (($token->token === ')') && ($brackets === 0)))
|
||||
) {
|
||||
// First pair of brackets is being skipped.
|
||||
$ret->options[$lastOptionId]['value'] .= $token->value;
|
||||
}
|
||||
|
||||
// Checking if we finished parsing.
|
||||
if ($brackets === 0) {
|
||||
@ -223,7 +229,9 @@ class OptionsArray extends Component
|
||||
}
|
||||
}
|
||||
|
||||
ksort($ret->options);
|
||||
if (empty($options['_UNSORTED'])) {
|
||||
ksort($ret->options);
|
||||
}
|
||||
|
||||
--$list->idx;
|
||||
return $ret;
|
||||
@ -245,9 +253,9 @@ class OptionsArray extends Component
|
||||
$options[] = $option;
|
||||
} else {
|
||||
$options[] = $option['name']
|
||||
. (!empty($option['equal']) ? '=' : ' ')
|
||||
. ((string) $option['value']);
|
||||
// If `$option['value']` happens to be a component, the magic
|
||||
. (!empty($option['equals']) ? '=' : ' ')
|
||||
. (!empty($option['expr']) ? ((string) $option['expr']) : $option['value']);
|
||||
// If `$option['expr']` happens to be a component, the magic
|
||||
// method will build it automatically.
|
||||
}
|
||||
}
|
||||
@ -257,17 +265,19 @@ class OptionsArray extends Component
|
||||
/**
|
||||
* Checks if it has the specified option and returns it value or true.
|
||||
*
|
||||
* @param string $key The key to be checked.
|
||||
* @param string $key The key to be checked.
|
||||
* @param bool $getExpr Gets the expression instead of the value.
|
||||
* The value is the processed form of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function has($key)
|
||||
public function has($key, $getExpr = false)
|
||||
{
|
||||
foreach ($this->options as $option) {
|
||||
if ($key === $option) {
|
||||
return true;
|
||||
} elseif ((is_array($option)) && ($key === $option['name'])) {
|
||||
return $option['value'];
|
||||
return $getExpr ? $option['expr'] : $option['value'];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@ -309,4 +319,14 @@ class OptionsArray extends Component
|
||||
$this->options = array_merge_recursive($this->options, $options->options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks tf there are no options set.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +120,6 @@ class ParameterDefinition extends Component
|
||||
$expr = new ParameterDefinition();
|
||||
if ($token->value === ',') {
|
||||
$state = 1;
|
||||
continue;
|
||||
} elseif ($token->value === ')') {
|
||||
++$list->idx;
|
||||
break;
|
||||
|
||||
@ -52,6 +52,13 @@ class RenameOperation extends Component
|
||||
|
||||
$expr = new RenameOperation();
|
||||
|
||||
/**
|
||||
* Whether an operation was parsed or not. To be a valid parsing, at
|
||||
* least one operation must be parsed after each comma.
|
||||
* @var bool $parsed
|
||||
*/
|
||||
$parsed = false;
|
||||
|
||||
/**
|
||||
* The state of the parser.
|
||||
*
|
||||
@ -87,29 +94,7 @@ class RenameOperation extends Component
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) {
|
||||
if (($state === 1) && ($token->value === 'TO')) {
|
||||
$state = 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// No other keyword is expected.
|
||||
break;
|
||||
}
|
||||
|
||||
if ($token->type === Token::TYPE_OPERATOR) {
|
||||
if (($state === 3) && ($token->value === ',')) {
|
||||
$ret[] = $expr;
|
||||
$expr = new RenameOperation();
|
||||
$state = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// No other operator is expected.
|
||||
break;
|
||||
}
|
||||
|
||||
if ($state == 0) {
|
||||
if ($state === 0) {
|
||||
$expr->old = Expression::parse(
|
||||
$parser,
|
||||
$list,
|
||||
@ -119,8 +104,18 @@ class RenameOperation extends Component
|
||||
'skipColumn' => true,
|
||||
)
|
||||
);
|
||||
if (empty($expr->old)) {
|
||||
$parser->error('The old name of the table was expected.', $token);
|
||||
}
|
||||
$state = 1;
|
||||
} elseif ($state == 2) {
|
||||
} elseif ($state === 1) {
|
||||
if (($token->type === Token::TYPE_KEYWORD) && ($token->value === 'TO')) {
|
||||
$state = 2;
|
||||
} else {
|
||||
$parser->error('Keyword "TO" was expected.', $token);
|
||||
break;
|
||||
}
|
||||
} elseif ($state === 2) {
|
||||
$expr->new = Expression::parse(
|
||||
$parser,
|
||||
$list,
|
||||
@ -130,9 +125,26 @@ class RenameOperation extends Component
|
||||
'noAlias' => true,
|
||||
)
|
||||
);
|
||||
if (empty($expr->new)) {
|
||||
$parser->error('The new name of the table was expected.', $token);
|
||||
}
|
||||
$state = 3;
|
||||
$parsed = true;
|
||||
} elseif ($state === 3) {
|
||||
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) {
|
||||
$ret[] = $expr;
|
||||
$expr = new RenameOperation();
|
||||
$state = 0;
|
||||
// Found a comma, looking for another operation.
|
||||
$parsed = false;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$parsed) {
|
||||
$parser->error('A rename operation was expected.', $list->tokens[$list->idx - 1]);
|
||||
}
|
||||
|
||||
// Last iteration was not saved.
|
||||
|
||||
@ -88,25 +88,23 @@ class SetOperation extends Component
|
||||
break;
|
||||
}
|
||||
|
||||
if ($token->type === Token::TYPE_OPERATOR) {
|
||||
if ($token->value === ',') {
|
||||
if ($state === 0) {
|
||||
if ($token->token === '=') {
|
||||
$state = 1;
|
||||
} else {
|
||||
$expr->column .= $token->token;
|
||||
}
|
||||
} elseif ($state === 1) {
|
||||
if ($token->token === ',') {
|
||||
$expr->column = trim($expr->column);
|
||||
$expr->value = trim($expr->value);
|
||||
$ret[] = $expr;
|
||||
$expr = new SetOperation();
|
||||
$state = 0;
|
||||
continue;
|
||||
} elseif ($token->value === '=') {
|
||||
$state = 1;
|
||||
continue;
|
||||
} else {
|
||||
$expr->value .= $token->token;
|
||||
}
|
||||
}
|
||||
|
||||
if ($state === 0) {
|
||||
$expr->column .= $token->token;
|
||||
} else { // } else if ($state === 1) {
|
||||
$expr->value .= $token->token;
|
||||
}
|
||||
}
|
||||
|
||||
// Last iteration was not saved.
|
||||
|
||||
@ -397,7 +397,9 @@ abstract class Context
|
||||
*/
|
||||
public static function isSeparator($str)
|
||||
{
|
||||
return !ctype_alnum($str) && $str !== '_';
|
||||
// NOTES: Only ASCII characters may be separators.
|
||||
// `~` is the last printable ASCII character.
|
||||
return ($str <= '~') && (!ctype_alnum($str)) && ($str !== '_');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -434,7 +436,7 @@ abstract class Context
|
||||
* Loads the context with the closest version to the one specified.
|
||||
*
|
||||
* The closest context is found by replacing last digits with zero until one
|
||||
* is loaded succesfully.
|
||||
* is loaded successfully.
|
||||
*
|
||||
* @see Context::load()
|
||||
*
|
||||
@ -448,13 +450,12 @@ abstract class Context
|
||||
/**
|
||||
* The number of replaces done by `preg_replace`.
|
||||
* This actually represents whether a new context was generated or not.
|
||||
* @var int
|
||||
* @var int $count
|
||||
*/
|
||||
$count = 0;
|
||||
|
||||
// As long as a new context can be generated, we try to laod it.
|
||||
// As long as a new context can be generated, we try to load it.
|
||||
do {
|
||||
$loaded = true;
|
||||
try {
|
||||
// Trying to load the new context.
|
||||
static::load($context);
|
||||
|
||||
@ -13,6 +13,23 @@ namespace SqlParser;
|
||||
|
||||
use SqlParser\Exceptions\LexerException;
|
||||
|
||||
if (!defined('USE_UTF_STRINGS')) {
|
||||
|
||||
/**
|
||||
* Forces usage of `UtfString` if the string is multibyte.
|
||||
* `UtfString` may be slower, but it gives better results.
|
||||
* @var bool
|
||||
*/
|
||||
define('USE_UTF_STRINGS', true);
|
||||
}
|
||||
|
||||
// Set internal character to UTF-8.
|
||||
// In previous versions of PHP (5.5 and older) the default internal encoding is
|
||||
// "ISO-8859-1".
|
||||
if ((defined('USE_UTF_STRINGS')) && (USE_UTF_STRINGS)) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs lexical analysis over a SQL statement and splits it in multiple
|
||||
* tokens.
|
||||
@ -149,11 +166,24 @@ class Lexer
|
||||
*/
|
||||
public function __construct($str, $strict = false)
|
||||
{
|
||||
// `strlen` is used instead of `mb_strlen` because the lexer needs to
|
||||
// parse each byte of the input.
|
||||
$len = ($str instanceof UtfString) ? $str->length() : strlen($str);
|
||||
|
||||
// For multi-byte strings, a new instance of `UtfString` is
|
||||
// initialized (only if `UtfString` usage is forced.
|
||||
if (!($str instanceof UtfString)) {
|
||||
if ((USE_UTF_STRINGS) && ($len != mb_strlen($str))) {
|
||||
$str = new UtfString($str);
|
||||
}
|
||||
}
|
||||
|
||||
$this->str = $str;
|
||||
$this->len = ($str instanceof UtfString) ?
|
||||
$str->length() : strlen($str);
|
||||
$this->len = ($str instanceof UtfString) ? $str->length() : $len;
|
||||
|
||||
$this->strict = $strict;
|
||||
|
||||
// Setting the delimiter.
|
||||
$this->delimiter = static::$DEFAULT_DELIMITER;
|
||||
|
||||
$this->lex();
|
||||
|
||||
@ -338,7 +338,7 @@ class Parser
|
||||
// Checking if it is a known statement that can be parsed.
|
||||
if (empty(static::$STATEMENT_PARSERS[$token->value])) {
|
||||
$this->error(
|
||||
'Unrecognized statement type "' . $token->value . '".',
|
||||
'Unrecognized statement type.',
|
||||
$token
|
||||
);
|
||||
// Skipping to the end of this statement.
|
||||
@ -376,6 +376,10 @@ class Parser
|
||||
&& ($lastStatement instanceof SelectStatement)
|
||||
&& ($stmt instanceof SelectStatement)
|
||||
) {
|
||||
/**
|
||||
* Last SELECT statement.
|
||||
* @var SelectStatement $lastStatement
|
||||
*/
|
||||
$lastStatement->union[] = $stmt;
|
||||
$inUnion = false;
|
||||
} else {
|
||||
|
||||
@ -244,7 +244,7 @@ abstract class Statement
|
||||
// There is no parser for this keyword and isn't the beginning
|
||||
// of a statement (so no options) either.
|
||||
$parser->error(
|
||||
'Unrecognized keyword "' . $token->value . '".',
|
||||
'Unrecognized keyword.',
|
||||
$token
|
||||
);
|
||||
continue;
|
||||
@ -300,8 +300,8 @@ abstract class Statement
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString()
|
||||
{
|
||||
return static::build($this);
|
||||
return $this->build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +287,15 @@ class CreateStatement extends Statement
|
||||
'skipColumn' => true,
|
||||
)
|
||||
);
|
||||
++$list->idx; // Skipping field.
|
||||
|
||||
if (empty($this->name)) {
|
||||
$parser->error(
|
||||
'The name of the entity was expected.',
|
||||
$list->tokens[$list->idx]
|
||||
);
|
||||
} else {
|
||||
++$list->idx; // Skipping field.
|
||||
}
|
||||
|
||||
if ($this->options->has('DATABASE')) {
|
||||
$this->entityOptions = OptionsArray::parse(
|
||||
@ -297,6 +305,12 @@ class CreateStatement extends Statement
|
||||
);
|
||||
} elseif ($this->options->has('TABLE')) {
|
||||
$this->fields = FieldDefinition::parse($parser, $list);
|
||||
if (empty($this->fields)) {
|
||||
$parser->error(
|
||||
'At least one field definition was expected.',
|
||||
$list->tokens[$list->idx]
|
||||
);
|
||||
}
|
||||
++$list->idx;
|
||||
|
||||
$this->entityOptions = OptionsArray::parse(
|
||||
@ -312,7 +326,7 @@ class CreateStatement extends Statement
|
||||
$token = $list->getNextOfType(Token::TYPE_KEYWORD);
|
||||
if ($token->value !== 'RETURNS') {
|
||||
$parser->error(
|
||||
'\'RETURNS\' keyword was expected.',
|
||||
'A \'RETURNS\' keyword was expected.',
|
||||
$token
|
||||
);
|
||||
} else {
|
||||
|
||||
@ -93,7 +93,7 @@ class UtfString implements \ArrayAccess
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return $offset < $this->charLen;
|
||||
return ($offset >= 0) && ($offset < $this->charLen);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,25 +190,12 @@ class UtfString implements \ArrayAccess
|
||||
return 3;
|
||||
} elseif ($byte < 248) {
|
||||
return 4;
|
||||
} elseif ($byte === 252) {
|
||||
} elseif ($byte < 252) {
|
||||
return 5; // unofficial
|
||||
}
|
||||
return 6; // unofficial
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of remaining characters.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function remaining()
|
||||
{
|
||||
if ($this->charIdx < $this->charLen) {
|
||||
return $this->charLen - $this->charIdx;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length in characters of the string.
|
||||
*
|
||||
@ -220,30 +207,12 @@ class UtfString implements \ArrayAccess
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values of the indexes.
|
||||
* Returns the contained string.
|
||||
*
|
||||
* @param int &$byte Reference to the byte index.
|
||||
* @param int &$char Reference to the character index.
|
||||
*
|
||||
* @return void
|
||||
* @return strin
|
||||
*/
|
||||
public function getIndexes(&$byte, &$char)
|
||||
public function __toString()
|
||||
{
|
||||
$byte = $this->byteIdx;
|
||||
$char = $this->charIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values of the indexes.
|
||||
*
|
||||
* @param int $byte The byte index.
|
||||
* @param int $char The character index.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setIndexes($byte = 0, $char = 0)
|
||||
{
|
||||
$this->byteIdx = $byte;
|
||||
$this->charIdx = $char;
|
||||
return $this->str;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
*/
|
||||
namespace SqlParser\Utils;
|
||||
|
||||
use SqlParser\Components\Expression;
|
||||
use SqlParser\Statements\SelectStatement;
|
||||
|
||||
/**
|
||||
|
||||
@ -125,7 +125,7 @@ class Table
|
||||
|
||||
if (($option = $field->options->has('AS'))) {
|
||||
$ret[$field->name]['generated'] = true;
|
||||
$ret[$field->name]['expr'] = $option->expr;
|
||||
$ret[$field->name]['expr'] = $option;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
lint.php
Normal file
38
lint.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Represents the interface between the linter and the query editor.
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
|
||||
define('PHPMYADMIN', true);
|
||||
|
||||
// We load the minimum files required to check if the user is logged in.
|
||||
require_once 'libraries/core.lib.php';
|
||||
require_once 'libraries/Config.class.php';
|
||||
$GLOBALS['PMA_Config'] = new PMA_Config(CONFIG_FILE);
|
||||
require_once 'libraries/session.inc.php';
|
||||
|
||||
// If user is not logged in, he should not send any requests, so we exit here to
|
||||
// avoid external requests.
|
||||
if (empty($_SESSION['encryption_key'])) {
|
||||
// Unauthorized access detected.
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the SQL lexer and parser, which are used to detect errors.
|
||||
*/
|
||||
require_once 'libraries/sql-parser/autoload.php';
|
||||
|
||||
/**
|
||||
* Loads the linter.
|
||||
*/
|
||||
require_once 'libraries/Linter.class.php';
|
||||
|
||||
// The input of this function does not need to be checked again XSS or MySQL
|
||||
// injections because it is never executed, just parsed.
|
||||
// The client, which will recieve the JSON response will decode the message and
|
||||
// and any HTML fragments that are displayed to the user will be encoded anyway.
|
||||
PMA_Linter::lint($_REQUEST['sql_query']);
|
||||
147
test/libraries/PMA_Linter_Test.php
Normal file
147
test/libraries/PMA_Linter_Test.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Tests for Linter.class.php.
|
||||
*
|
||||
* @package PhpMyAdmin-test
|
||||
*/
|
||||
|
||||
/*
|
||||
* Include to test.
|
||||
*/
|
||||
require_once 'libraries/Linter.class.php';
|
||||
|
||||
/**
|
||||
* Tests for Linter.class.php.
|
||||
*
|
||||
* @package PhpMyAdmin-test
|
||||
*/
|
||||
class PMA_Linter_Test extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::getLines
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testGetLines()
|
||||
{
|
||||
$this->assertEquals(array(0), PMA_Linter::getLines(''));
|
||||
$this->assertEquals(array(0, 2), PMA_Linter::getLines("a\nb"));
|
||||
$this->assertEquals(array(0, 4, 7), PMA_Linter::getLines("abc\nde\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::findLineNumberAndColumn
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testFindLineNumberAndColumn()
|
||||
{
|
||||
// Let the analyzed string be:
|
||||
// ^abc$
|
||||
// ^de$
|
||||
// ^$
|
||||
//
|
||||
// Where `^` is the beginning of the line and `$` the end of the line.
|
||||
//
|
||||
// Positions of each character (by line):
|
||||
// ( a, 0), ( b, 1), ( c, 2), (\n, 3),
|
||||
// ( d, 4), ( e, 5), (\n, 6),
|
||||
// (\n, 7).
|
||||
$this->assertEquals(
|
||||
array(1, 0),
|
||||
PMA_Linter::findLineNumberAndColumn(array(0, 4, 7), 4)
|
||||
);
|
||||
$this->assertEquals(
|
||||
array(1, 1),
|
||||
PMA_Linter::findLineNumberAndColumn(array(0, 4, 7), 5)
|
||||
);
|
||||
$this->assertEquals(
|
||||
array(1, 2),
|
||||
PMA_Linter::findLineNumberAndColumn(array(0, 4, 7), 6)
|
||||
);
|
||||
$this->assertEquals(
|
||||
array(2, 0),
|
||||
PMA_Linter::findLineNumberAndColumn(array(0, 4, 7), 7)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::lint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testLintEmpty()
|
||||
{
|
||||
$this->expectOutputString('[]');
|
||||
PMA_Linter::lint('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::lint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testLintNoErrors()
|
||||
{
|
||||
$this->expectOutputString('[]');
|
||||
PMA_Linter::lint('SELECT * FROM tbl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::lint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testLintErrors()
|
||||
{
|
||||
$this->expectOutputString(
|
||||
json_encode(
|
||||
array(
|
||||
array(
|
||||
'message' => 'Unrecognized data type. (near <code>IN</code>)',
|
||||
'fromLine' => 0,
|
||||
'fromColumn' => 22,
|
||||
'toLine' => 0,
|
||||
'toColumn' => 24,
|
||||
'severity' => 'error',
|
||||
),
|
||||
array(
|
||||
'message' => 'A closing bracket was expected. (near <code>IN</code>)',
|
||||
'fromLine' => 0,
|
||||
'fromColumn' => 22,
|
||||
'toLine' => 0,
|
||||
'toColumn' => 24,
|
||||
'severity' => 'error',
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
PMA_Linter::lint('CREATE TABLE tbl ( id IN');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for PMA_Linter::lint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testLongQuery()
|
||||
{
|
||||
$this->expectOutputString(
|
||||
json_encode(
|
||||
array(
|
||||
array(
|
||||
'message' => 'The linting is disabled for this query because it exceededs the maxmimum length.',
|
||||
'fromLine' => 0,
|
||||
'fromColumn' => 0,
|
||||
'toLine' => 0,
|
||||
'toColumn' => 0,
|
||||
'severity' => 'warning',
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
PMA_Linter::lint(str_repeat(";", 10001));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user