Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/phpmyadmin/phpmyadmin.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Delisle <marc@infomarc.info>2015-07-19 00:54:40 +0300
committerMarc Delisle <marc@infomarc.info>2015-07-19 00:54:40 +0300
commite4d43490248a71ec50df772cfc32f9e00c09b8ea (patch)
treec4f8e2a62ae28ffea0bd095426ae1e398c974738
parentfec98e1450541c765c837343cdaff7987565143e (diff)
parentf0667aa6633ef4ac645a58630ab8ebb80a327869 (diff)
Merge pull request #1788 from udan11/lint
[WIP] Added lint.
-rw-r--r--js/codemirror/addon/lint/lint.css77
-rw-r--r--js/codemirror/addon/lint/lint.js209
-rw-r--r--js/codemirror/addon/lint/sql-lint.js37
-rw-r--r--js/functions.js11
-rw-r--r--libraries/Header.class.php6
-rw-r--r--libraries/Linter.class.php162
-rw-r--r--libraries/config.default.php7
-rw-r--r--libraries/config/messages.inc.php3
-rw-r--r--libraries/config/setup.forms.php1
-rw-r--r--libraries/config/user_preferences.forms.php1
-rw-r--r--libraries/sql-parser/src/Components/AlterOperation.php22
-rw-r--r--libraries/sql-parser/src/Components/Array2d.php85
-rw-r--r--libraries/sql-parser/src/Components/ArrayObj.php4
-rw-r--r--libraries/sql-parser/src/Components/Expression.php44
-rw-r--r--libraries/sql-parser/src/Components/FieldDefinition.php46
-rw-r--r--libraries/sql-parser/src/Components/Limit.php4
-rw-r--r--libraries/sql-parser/src/Components/OptionsArray.php56
-rw-r--r--libraries/sql-parser/src/Components/ParameterDefinition.php1
-rw-r--r--libraries/sql-parser/src/Components/RenameOperation.php60
-rw-r--r--libraries/sql-parser/src/Components/SetOperation.php22
-rw-r--r--libraries/sql-parser/src/Context.php11
-rw-r--r--libraries/sql-parser/src/Lexer.php34
-rw-r--r--libraries/sql-parser/src/Parser.php6
-rw-r--r--libraries/sql-parser/src/Statement.php6
-rw-r--r--libraries/sql-parser/src/Statements/CreateStatement.php18
-rw-r--r--libraries/sql-parser/src/UtfString.php43
-rw-r--r--libraries/sql-parser/src/Utils/Misc.php1
-rw-r--r--libraries/sql-parser/src/Utils/Table.php2
-rw-r--r--lint.php38
-rw-r--r--test/libraries/PMA_Linter_Test.php147
30 files changed, 973 insertions, 191 deletions
diff --git a/js/codemirror/addon/lint/lint.css b/js/codemirror/addon/lint/lint.css
new file mode 100644
index 0000000000..ca59a6f1ff
--- /dev/null
+++ b/js/codemirror/addon/lint/lint.css
@@ -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%;
+}
diff --git a/js/codemirror/addon/lint/lint.js b/js/codemirror/addon/lint/lint.js
new file mode 100644
index 0000000000..fc4a636320
--- /dev/null
+++ b/js/codemirror/addon/lint/lint.js
@@ -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("&lt;code&gt;", "<code>")
+ .replace("&lt;/code&gt;", "</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);
+ }
+ });
+});
diff --git a/js/codemirror/addon/lint/sql-lint.js b/js/codemirror/addon/lint/sql-lint.js
new file mode 100644
index 0000000000..557d3a478c
--- /dev/null
+++ b/js/codemirror/addon/lint/sql-lint.js
@@ -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
+ });
+}
diff --git a/js/functions.js b/js/functions.js
index 531994079a..b889eeedee 100644
--- a/js/functions.js
+++ b/js/functions.js
@@ -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
diff --git a/libraries/Header.class.php b/libraries/Header.class.php
index f138679be1..00da50a661 100644
--- a/libraries/Header.class.php
+++ b/libraries/Header.class.php
@@ -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='
@@ -659,6 +663,8 @@ class PMA_Header
$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'] . '" />';
// load Print view's CSS last, so that it overrides all other CSS while 'printing'
diff --git a/libraries/Linter.class.php b/libraries/Linter.class.php
new file mode 100644
index 0000000000..5315ce479f
--- /dev/null
+++ b/libraries/Linter.class.php
@@ -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);
+ }
+
+} \ No newline at end of file
diff --git a/libraries/config.default.php b/libraries/config.default.php
index 4a5195d442..7e5c6c8ec3 100644
--- a/libraries/config.default.php
+++ b/libraries/config.default.php
@@ -758,6 +758,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
*
* @global boolean $cfg['AllowUserDropDatabase']
diff --git a/libraries/config/messages.inc.php b/libraries/config/messages.inc.php
index 583678ff13..d70b19706e 100644
--- a/libraries/config/messages.inc.php
+++ b/libraries/config/messages.inc.php
@@ -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.'
diff --git a/libraries/config/setup.forms.php b/libraries/config/setup.forms.php
index 304919c940..5ab03c8b27 100644
--- a/libraries/config/setup.forms.php
+++ b/libraries/config/setup.forms.php
@@ -162,6 +162,7 @@ $forms['Sql_queries']['Sql_queries'] = array(
'MaxCharactersInDisplayedSQL',
'RetainQueryBox',
'CodemirrorEnable',
+ 'LintEnable',
'EnableAutocompleteForTablesAndColumns',
'DefaultForeignKeyChecks');
$forms['Sql_queries']['Sql_box'] = array('SQLQuery' => array(
diff --git a/libraries/config/user_preferences.forms.php b/libraries/config/user_preferences.forms.php
index c150905a6e..d51acd25d0 100644
--- a/libraries/config/user_preferences.forms.php
+++ b/libraries/config/user_preferences.forms.php
@@ -70,6 +70,7 @@ $forms['Sql_queries']['Sql_queries'] = array(
'MaxCharactersInDisplayedSQL',
'RetainQueryBox',
'CodemirrorEnable',
+ 'LintEnable',
'EnableAutocompleteForTablesAndColumns',
'DefaultForeignKeyChecks');
$forms['Sql_queries']['Sql_box'] = array(
diff --git a/libraries/sql-parser/src/Components/AlterOperation.php b/libraries/sql-parser/src/Components/AlterOperation.php
index 7c528283be..9f859e2787 100644
--- a/libraries/sql-parser/src/Components/AlterOperation.php
+++ b/libraries/sql-parser/src/Components/AlterOperation.php
@@ -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;
}
diff --git a/libraries/sql-parser/src/Components/Array2d.php b/libraries/sql-parser/src/Components/Array2d.php
index c521de2a16..117ad75624 100644
--- a/libraries/sql-parser/src/Components/Array2d.php
+++ b/libraries/sql-parser/src/Components/Array2d.php
@@ -26,39 +26,38 @@ 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;
diff --git a/libraries/sql-parser/src/Components/ArrayObj.php b/libraries/sql-parser/src/Components/ArrayObj.php
index 01571defa3..d38077c88d 100644
--- a/libraries/sql-parser/src/Components/ArrayObj.php
+++ b/libraries/sql-parser/src/Components/ArrayObj.php
@@ -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 === ',') {
diff --git a/libraries/sql-parser/src/Components/Expression.php b/libraries/sql-parser/src/Components/Expression.php
index 065810c523..964ceb931c 100644
--- a/libraries/sql-parser/src/Components/Expression.php
+++ b/libraries/sql-parser/src/Components/Expression.php
@@ -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;
- } else {
- if (!empty($ret->column)) {
- break;
+
+ // 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;
}
- $ret->column = $token->value;
+ } else {
+ $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.
diff --git a/libraries/sql-parser/src/Components/FieldDefinition.php b/libraries/sql-parser/src/Components/FieldDefinition.php
index 0217964068..4df98cb2a5 100644
--- a/libraries/sql-parser/src/Components/FieldDefinition.php
+++ b/libraries/sql-parser/src/Components/FieldDefinition.php
@@ -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
*/
diff --git a/libraries/sql-parser/src/Components/Limit.php b/libraries/sql-parser/src/Components/Limit.php
index 1e25d2aedd..86ab55afb7 100644
--- a/libraries/sql-parser/src/Components/Limit.php
+++ b/libraries/sql-parser/src/Components/Limit.php
@@ -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;
diff --git a/libraries/sql-parser/src/Components/OptionsArray.php b/libraries/sql-parser/src/Components/OptionsArray.php
index aabb807488..487eefd4d2 100644
--- a/libraries/sql-parser/src/Components/OptionsArray.php
+++ b/libraries/sql-parser/src/Components/OptionsArray.php
@@ -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);
+ }
}
diff --git a/libraries/sql-parser/src/Components/ParameterDefinition.php b/libraries/sql-parser/src/Components/ParameterDefinition.php
index db0f815c1e..d10a460f38 100644
--- a/libraries/sql-parser/src/Components/ParameterDefinition.php
+++ b/libraries/sql-parser/src/Components/ParameterDefinition.php
@@ -120,7 +120,6 @@ class ParameterDefinition extends Component
$expr = new ParameterDefinition();
if ($token->value === ',') {
$state = 1;
- continue;
} elseif ($token->value === ')') {
++$list->idx;
break;
diff --git a/libraries/sql-parser/src/Components/RenameOperation.php b/libraries/sql-parser/src/Components/RenameOperation.php
index 335043791a..9a91f81b8f 100644
--- a/libraries/sql-parser/src/Components/RenameOperation.php
+++ b/libraries/sql-parser/src/Components/RenameOperation.php
@@ -53,6 +53,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.
*
* Below are the states 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.
diff --git a/libraries/sql-parser/src/Components/SetOperation.php b/libraries/sql-parser/src/Components/SetOperation.php
index dad91e5425..a9e8ba1624 100644
--- a/libraries/sql-parser/src/Components/SetOperation.php
+++ b/libraries/sql-parser/src/Components/SetOperation.php
@@ -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.
diff --git a/libraries/sql-parser/src/Context.php b/libraries/sql-parser/src/Context.php
index 7f937dc2c4..dd912fb279 100644
--- a/libraries/sql-parser/src/Context.php
+++ b/libraries/sql-parser/src/Context.php
@@ -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);
diff --git a/libraries/sql-parser/src/Lexer.php b/libraries/sql-parser/src/Lexer.php
index d70641fbdb..aae95f1a58 100644
--- a/libraries/sql-parser/src/Lexer.php
+++ b/libraries/sql-parser/src/Lexer.php
@@ -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();
diff --git a/libraries/sql-parser/src/Parser.php b/libraries/sql-parser/src/Parser.php
index 260ea25a53..f4763063f2 100644
--- a/libraries/sql-parser/src/Parser.php
+++ b/libraries/sql-parser/src/Parser.php
@@ -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 {
diff --git a/libraries/sql-parser/src/Statement.php b/libraries/sql-parser/src/Statement.php
index 3739310224..4eb3ccbe63 100644
--- a/libraries/sql-parser/src/Statement.php
+++ b/libraries/sql-parser/src/Statement.php
@@ -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();
}
}
diff --git a/libraries/sql-parser/src/Statements/CreateStatement.php b/libraries/sql-parser/src/Statements/CreateStatement.php
index 8475448b41..afabc6a8c5 100644
--- a/libraries/sql-parser/src/Statements/CreateStatement.php
+++ b/libraries/sql-parser/src/Statements/CreateStatement.php
@@ -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 {
diff --git a/libraries/sql-parser/src/UtfString.php b/libraries/sql-parser/src/UtfString.php
index 27e3f93008..1e863a4546 100644
--- a/libraries/sql-parser/src/UtfString.php
+++ b/libraries/sql-parser/src/UtfString.php
@@ -93,7 +93,7 @@ class UtfString implements \ArrayAccess
*/
public function offsetExists($offset)
{
- return $offset < $this->charLen;
+ return ($offset >= 0) && ($offset < $this->charLen);
}
/**
@@ -190,26 +190,13 @@ 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.
*
* @return int
@@ -220,30 +207,12 @@ class UtfString implements \ArrayAccess
}
/**
- * Gets the values of the indexes.
- *
- * @param int &$byte Reference to the byte index.
- * @param int &$char Reference to the character index.
- *
- * @return void
- */
- public function getIndexes(&$byte, &$char)
- {
- $byte = $this->byteIdx;
- $char = $this->charIdx;
- }
-
- /**
- * Sets the values of the indexes.
+ * Returns the contained string.
*
- * @param int $byte The byte index.
- * @param int $char The character index.
- *
- * @return void
+ * @return strin
*/
- public function setIndexes($byte = 0, $char = 0)
+ public function __toString()
{
- $this->byteIdx = $byte;
- $this->charIdx = $char;
+ return $this->str;
}
}
diff --git a/libraries/sql-parser/src/Utils/Misc.php b/libraries/sql-parser/src/Utils/Misc.php
index 9d3dfa99f8..2107172317 100644
--- a/libraries/sql-parser/src/Utils/Misc.php
+++ b/libraries/sql-parser/src/Utils/Misc.php
@@ -8,6 +8,7 @@
*/
namespace SqlParser\Utils;
+use SqlParser\Components\Expression;
use SqlParser\Statements\SelectStatement;
/**
diff --git a/libraries/sql-parser/src/Utils/Table.php b/libraries/sql-parser/src/Utils/Table.php
index 6b17457d3a..c5ce2fe9db 100644
--- a/libraries/sql-parser/src/Utils/Table.php
+++ b/libraries/sql-parser/src/Utils/Table.php
@@ -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;
}
}
diff --git a/lint.php b/lint.php
new file mode 100644
index 0000000000..b1d4bb414c
--- /dev/null
+++ b/lint.php
@@ -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']);
diff --git a/test/libraries/PMA_Linter_Test.php b/test/libraries/PMA_Linter_Test.php
new file mode 100644
index 0000000000..56667170a5
--- /dev/null
+++ b/test/libraries/PMA_Linter_Test.php
@@ -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));
+ }
+}