diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c1cb757 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +.nyc_output/ +coverage/ diff --git a/.eslintrc b/.eslintrc index c1045a1..b9a8e31 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,11 +2,31 @@ "env": { "node": true }, + "plugins": [ + "markdown" + ], + "overrides": [ + { + "files": "**/*.md", + "processor": "markdown/markdown" + }, + { + "files": "**/*.md/*.js", + "rules": { + "no-undef": 0, + "no-unused-vars": 0 + } + } + ], "rules": { "comma-dangle": [2, "never"], + "comma-spacing": ["error", { "before": false, "after": true }], "consistent-return": 2, - "indent": ["error", 2, {"SwitchCase": 1}], + "eqeqeq": [2, "allow-null"], + "indent": [2, 2, { "VariableDeclarator": 2, "SwitchCase": 1 }], "key-spacing": [2, { "align": { "beforeColon": true, "afterColon": true, "on": "colon" } }], + "keyword-spacing": 2, + "new-parens": 2, "no-cond-assign": 2, "no-constant-condition": 2, "no-control-regex": 2, @@ -23,15 +43,20 @@ "no-inner-declarations": 2, "no-invalid-regexp": 2, "no-irregular-whitespace": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], "no-negated-in-lhs": 2, "no-obj-calls": 2, "no-regex-spaces": 2, "no-sparse-arrays": 2, "no-trailing-spaces": 2, + "no-undef": 2, "no-unexpected-multiline": 2, "no-unreachable": 2, "no-unused-vars": 2, + "one-var": ["error", { "initialized": "never" }], + "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "semi": [2, "always"], + "semi-spacing": 2, "space-infix-ops": 2, "use-isnan": 2, "valid-jsdoc": 2, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9b344e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,188 @@ +name: ci + +on: +- pull_request +- push + +jobs: + test: + runs-on: ubuntu-18.04 + strategy: + matrix: + name: + - Node.js 0.6 + - Node.js 0.8 + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + - Node.js 15.x + - Node.js 16.x + - Node.js 17.x + + include: + - name: Node.js 0.6 + node-version: "0.6" + + - name: Node.js 0.8 + node-version: "0.8" + + - name: Node.js 0.10 + node-version: "0.10" + + - name: Node.js 0.12 + node-version: "0.12" + + - name: io.js 1.x + node-version: "1.8" + + - name: io.js 2.x + node-version: "2.5" + + - name: io.js 3.x + node-version: "3.3" + + - name: Node.js 4.x + node-version: "4.9" + + - name: Node.js 5.x + node-version: "5.12" + + - name: Node.js 6.x + node-version: "6.17" + + - name: Node.js 7.x + node-version: "7.10" + + - name: Node.js 8.x + node-version: "8.17" + + - name: Node.js 9.x + node-version: "9.11" + + - name: Node.js 10.x + node-version: "10.24" + + - name: Node.js 11.x + node-version: "11.15" + + - name: Node.js 12.x + node-version: "12.22" + + - name: Node.js 13.x + node-version: "13.14" + + - name: Node.js 14.x + node-version: "14.19" + + - name: Node.js 15.x + node-version: "15.14" + + - name: Node.js 16.x + node-version: "16.14" + + - name: Node.js 17.x + node-version: "17.6" + + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + if [[ "${{ matrix.node-version }}" == 0.6* ]]; then + sudo apt-get install g++-4.8 gcc-4.8 libssl1.0-dev + export CC=/usr/bin/gcc-4.8 + export CXX=/usr/bin/g++-4.8 + fi + nvm install --default ${{ matrix.node-version }} + if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + nvm install --alias=npm 0.10 + nvm use ${{ matrix.node-version }} + if [[ "$(npm -v)" == 1.1.* ]]; then + nvm exec npm npm install -g npm@1.1 + ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" + else + sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" + fi + npm config set strict-ssl false + fi + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: npm config set shrinkwrap false + + - name: Remove non-test npm modules + run: npm rm --silent --save-dev benchmark beautify-benchmark + + - name: Setup Node.js version-specific dependencies + shell: bash + run: | + # eslint for linting + # - remove on Node.js < 12 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + sort -r | \ + xargs -n1 npm rm --silent --save-dev + fi + # nyc for coverage + # - remove on Node.js < 8 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 8 ]]; then + npm rm --silent --save-dev nyc + fi + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + + - name: Run tests + shell: bash + run: | + if npm -ps ls nyc | grep -q nyc; then + npm run test-ci + else + npm test + fi + + - name: Lint code + if: steps.list_env.outputs.eslint != '' + run: npm run lint + + - name: Collect code coverage + uses: coverallsapp/github-action@master + if: steps.list_env.outputs.nyc != '' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-${{ matrix.test_number }} + parallel: true + + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Upload code coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.gitignore b/.gitignore index 25e8bbd..207febb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage node_modules npm-debug.log +package-lock.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8b8dd04..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: node_js -node_js: - - "0.6" - - "0.8" - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.8" - - "5.12" - - "6.10" - - "7.10" -sudo: false -cache: - directories: - - node_modules -before_install: - # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.6' || npm rm --save-dev nyc" - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev nyc" - - "test $(echo $TRAVIS_NODE_VERSION | cut -d'.' -f1) -ge 4 || npm rm --save-dev eslint eslint-plugin-markdown" - # Update Node.js modules - - "test ! -d node_modules || npm prune" - - "test ! -d node_modules || npm rebuild" -script: - # Run test script, depending on nyc install - - "test ! -z $(npm -ps ls nyc) || npm test" - - "test -z $(npm -ps ls nyc) || npm run-script test-ci" - # Run linter - - "test -z $(npm -ps ls eslint) || npm run-script lint" -after_script: - - "test -d .nyc_output && npm install coveralls@2 && nyc report --reporter=text-lcov | coveralls" diff --git a/HISTORY.md b/HISTORY.md index 1a0e0e3..aea1dfc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,25 @@ +2.3.3 / 2022-03-06 +================== + + * Fix escaping `Date` objects from foreign isolates + +2.3.2 / 2020-04-15 +================== + + * perf: remove outdated array pattern + +2.3.1 / 2018-02-24 +================== + + * Fix incorrectly replacing non-placeholders in SQL + +2.3.0 / 2017-10-01 +================== + + * Add `.toSqlString()` escape overriding + * Add `raw` method to wrap raw strings for escape overriding + * Small performance improvement on `escapeId` + 2.2.0 / 2016-11-01 ================== @@ -18,11 +40,12 @@ ================== * Bring repository up-to-date with `mysql` module changes + * Support Node.js 0.6.x 1.0.0 / 2014-11-09 ================== - * build: support Node.js 0.8.x + * Support Node.js 0.8.x 0.0.1 / 2014-02-25 ================== diff --git a/README.md b/README.md index e9f9ca4..5a9934f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![NPM Version][npm-version-image]][npm-url] [![NPM Downloads][npm-downloads-image]][npm-url] [![Node.js Version][node-image]][node-url] -[![Build Status][travis-image]][travis-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] [![Coverage Status][coveralls-image]][coveralls-url] Simple SQL escape and format for MySQL @@ -16,7 +16,6 @@ $ npm install sqlstring ## Usage - ```js var SqlString = require('sqlstring'); @@ -24,6 +23,22 @@ var SqlString = require('sqlstring'); ### Escaping query values +**Caution** These methods of escaping values only works when the +[NO_BACKSLASH_ESCAPES](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_backslash_escapes) +SQL mode is disabled (which is the default state for MySQL servers). + +**Caution** This library performs client-side escaping, as this is a library +to generate SQL strings on the client side. The syntax for functions like +`SqlString.format` may look similar to a prepared statement, but it is not +and the escaping rules from this module are used to generate a resulting SQL +string. The purpose of escaping input is to avoid SQL Injection attacks. +In order to support enhanced support like `SET` and `IN` formatting, this +module will escape based on the shape of the passed in JavaScript value, +and the resulting escaped string may be more than a single value. When +structured user input is provided as the value to escape, care should be taken +to validate the shape of the input to validate the output will be what is +expected. + In order to avoid SQL Injection attacks, you should always escape any user provided data before using it inside a SQL query. You can do so using the `SqlString.escape()` method: @@ -70,6 +85,8 @@ Different value types are escaped differently, here is how: * Arrays are turned into list, e.g. `['a', 'b']` turns into `'a', 'b'` * Nested arrays are turned into grouped lists (for bulk inserts), e.g. `[['a', 'b'], ['c', 'd']]` turns into `('a', 'b'), ('c', 'd')` +* Objects that have a `toSqlString` method will have `.toSqlString()` called + and the returned value is used as the raw SQL. * Objects are turned into `key = 'val'` pairs for each enumerable property on the object. If the property's value is a function, it is skipped; if the property's value is an object, toString() is called on it and the returned @@ -79,8 +96,7 @@ Different value types are escaped differently, here is how: to insert them as values will trigger MySQL errors until they implement support. -If you paid attention, you may have noticed that this escaping allows you -to do neat things like this: +You may have noticed that this escaping allows you to do neat things like this: ```js var post = {id: 1, title: 'Hello MySQL'}; @@ -88,11 +104,32 @@ var sql = SqlString.format('INSERT INTO posts SET ?', post); console.log(sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL' ``` +And the `toSqlString` method allows you to form complex queries with functions: + +```js +var CURRENT_TIMESTAMP = { toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }; +var sql = SqlString.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]); +console.log(sql); // UPDATE posts SET modified = CURRENT_TIMESTAMP() WHERE id = 42 +``` + +To generate objects with a `toSqlString` method, the `SqlString.raw()` method can +be used. This creates an object that will be left un-touched when using in a `?` +placeholder, useful for using functions as dynamic values: + +**Caution** The string provided to `SqlString.raw()` will skip all escaping +functions when used, so be careful when passing in unvalidated input. + +```js +var CURRENT_TIMESTAMP = SqlString.raw('CURRENT_TIMESTAMP()'); +var sql = SqlString.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]); +console.log(sql); // UPDATE posts SET modified = CURRENT_TIMESTAMP() WHERE id = 42 +``` + If you feel the need to escape queries by yourself, you can also use the escaping function directly: ```js -var sql = 'SELECT * FROM posts WHERE title=' + SqlString.escape("Hello MySQL"); +var sql = 'SELECT * FROM posts WHERE title=' + SqlString.escape('Hello MySQL'); console.log(sql); // SELECT * FROM posts WHERE title='Hello MySQL' ``` @@ -155,6 +192,16 @@ You also have the option (but are not required) to pass in `stringifyObject` and allowing you provide a custom means of turning objects into strings, as well as a location-specific/timezone-aware `Date`. +This can be further combined with the `SqlString.raw()` helper to generate SQL +that includes MySQL functions as dynamic vales: + +```js +var userId = 1; +var data = { email: 'foobar@example.com', modified: SqlString.raw('NOW()') }; +var sql = SqlString.format('UPDATE ?? SET ? WHERE `id` = ?', ['users', data, userId]); +console.log(sql); // UPDATE `users` SET `email` = 'foobar@example.com', `modified` = NOW() WHERE `id` = 1 +``` + ## License [MIT](LICENSE) @@ -162,9 +209,9 @@ location-specific/timezone-aware `Date`. [npm-version-image]: https://img.shields.io/npm/v/sqlstring.svg [npm-downloads-image]: https://img.shields.io/npm/dm/sqlstring.svg [npm-url]: https://npmjs.org/package/sqlstring -[travis-image]: https://img.shields.io/travis/mysqljs/sqlstring/master.svg -[travis-url]: https://travis-ci.org/mysqljs/sqlstring [coveralls-image]: https://img.shields.io/coveralls/mysqljs/sqlstring/master.svg [coveralls-url]: https://coveralls.io/r/mysqljs/sqlstring?branch=master +[github-actions-ci-image]: https://img.shields.io/github/workflow/status/mysqljs/sqlstring/ci/master?label=build +[github-actions-ci-url]: https://github.com/mysqljs/sqlstring/actions/workflows/ci.yml [node-image]: https://img.shields.io/node/v/sqlstring.svg [node-url]: https://nodejs.org/en/download diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..53d27d9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policies and Procedures + +## Reporting a Bug + +The `sqlstring` team and community take all security bugs seriously. Thank you +for improving the security of this module. Your efforts and responsible disclosure +and every effort will be made to acknowledge your contributions, as long as they +were responsibility disclosed. + +Report security bugs by emailing the current owners of `sqlstring`. This information +can be found in the npm registry using the command `npm owner ls sqlstring`. +If unsure or unable to get the information from the above, open an issue +in the [project issue tracker](https://github.com/mysqljs/sqlstring/issues) +asking for the current contact information. + +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + +At least one owner will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the owners will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. diff --git a/benchmark/escape.js b/benchmark/escape.js new file mode 100644 index 0000000..c195de4 --- /dev/null +++ b/benchmark/escape.js @@ -0,0 +1,76 @@ +var benchmark = require('benchmark'); +var benchmarks = require('beautify-benchmark'); + +global.SqlString = require('..'); + +global.arr = [ 42, 'foobar' ]; +global.buf = new Buffer('foobar'); +global.date = new Date(0); +global.func = { toSqlString: function () { return 'NOW()'; } }; +global.num = 42; +global.obj = { foo: 'bar' }; +global.str = 'foobar'; + +var suite = new benchmark.Suite(); + +suite.add({ + name : 'array', + minSamples : 100, + fn : 'var val = SqlString.escape(arr);' +}); + +suite.add({ + name : 'boolean', + minSamples : 100, + fn : 'var val = SqlString.escape(true);' +}); + +suite.add({ + name : 'date', + minSamples : 100, + fn : 'var val = SqlString.escape(date);' +}); + +suite.add({ + name : 'function', + minSamples : 100, + fn : 'var val = SqlString.escape(func);' +}); + +suite.add({ + name : 'null', + minSamples : 100, + fn : 'var val = SqlString.escape(null);' +}); + +suite.add({ + name : 'number', + minSamples : 100, + fn : 'var val = SqlString.escape(num);' +}); + +suite.add({ + name : 'object', + minSamples : 100, + fn : 'var val = SqlString.escape(obj);' +}); + +suite.add({ + name : 'string', + minSamples : 100, + fn : 'var val = SqlString.escape(str);' +}); + +suite.on('start', function onCycle() { + process.stdout.write(' escape\n\n'); +}); + +suite.on('cycle', function onCycle(event) { + benchmarks.add(event.target); +}); + +suite.on('complete', function onComplete() { + benchmarks.log(); +}); + +suite.run({async: false}); diff --git a/benchmark/escapeId.js b/benchmark/escapeId.js new file mode 100644 index 0000000..3113bd0 --- /dev/null +++ b/benchmark/escapeId.js @@ -0,0 +1,38 @@ +var benchmark = require('benchmark'); +var benchmarks = require('beautify-benchmark'); + +global.SqlString = require('..'); + +var suite = new benchmark.Suite(); + +suite.add({ + name : '"col"', + minSamples : 100, + fn : 'var val = SqlString.escapeId("col");' +}); + +suite.add({ + name : '"tbl.col"', + minSamples : 100, + fn : 'var val = SqlString.escapeId("tbl.col");' +}); + +suite.add({ + name : '["col1", "col2"]', + minSamples : 100, + fn : 'var val = SqlString.escapeId(["col1", "col2"]);' +}); + +suite.on('start', function onCycle() { + process.stdout.write(' escapeId\n\n'); +}); + +suite.on('cycle', function onCycle(event) { + benchmarks.add(event.target); +}); + +suite.on('complete', function onComplete() { + benchmarks.log(); +}); + +suite.run({async: false}); diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 0000000..5c16f5e --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,39 @@ +var fs = require('fs'); +var path = require('path'); +var spawn = require('child_process').spawn; + +var exe = process.argv[0]; +var cwd = process.cwd(); + +for (var dep in process.versions) { + console.log(' %s@%s', dep, process.versions[dep]); +} + +console.log(''); + +runScripts(fs.readdirSync(__dirname)); + +function runScripts(fileNames) { + var fileName = fileNames.shift(); + + if (!fileName) { + return; + } + + if (!/\.js$/i.test(fileName) || fileName.toLowerCase() === 'index.js') { + runScripts(fileNames); + return; + } + + var fullPath = path.join(__dirname, fileName); + + console.log('> %s %s', exe, path.relative(cwd, fullPath)); + + var proc = spawn(exe, [fullPath], { + 'stdio': 'inherit' + }); + + proc.on('exit', function () { + runScripts(fileNames); + }); +} diff --git a/lib/SqlString.js b/lib/SqlString.js index 53e08b3..8206dad 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -1,6 +1,9 @@ var SqlString = exports; -var charsRegex = /[\0\b\t\n\r\x1a\"\'\\]/g; // eslint-disable-line no-control-regex -var charsMap = { + +var ID_GLOBAL_REGEXP = /`/g; +var QUAL_GLOBAL_REGEXP = /\./g; +var CHARS_GLOBAL_REGEXP = /[\0\b\t\n\r\x1a\"\'\\]/g; // eslint-disable-line no-control-regex +var CHARS_ESCAPE_MAP = { '\0' : '\\0', '\b' : '\\b', '\t' : '\\t', @@ -21,13 +24,11 @@ SqlString.escapeId = function escapeId(val, forbidQualified) { } return sql; + } else if (forbidQualified) { + return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; + } else { + return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``').replace(QUAL_GLOBAL_REGEXP, '`.`') + '`'; } - - if (forbidQualified) { - return '`' + String(val).replace(/`/g, '``') + '`'; - } - - return '`' + String(val).replace(/`/g, '``').replace(/\./g, '`.`') + '`'; }; SqlString.escape = function escape(val, stringifyObjects, timeZone) { @@ -39,12 +40,14 @@ SqlString.escape = function escape(val, stringifyObjects, timeZone) { case 'boolean': return (val) ? 'true' : 'false'; case 'number': return val + ''; case 'object': - if (val instanceof Date) { + if (Object.prototype.toString.call(val) === '[object Date]') { return SqlString.dateToString(val, timeZone || 'local'); } else if (Array.isArray(val)) { return SqlString.arrayToList(val, timeZone); } else if (Buffer.isBuffer(val)) { return SqlString.bufferToString(val); + } else if (typeof val.toSqlString === 'function') { + return String(val.toSqlString()); } else if (stringifyObjects) { return escapeString(val.toString()); } else { @@ -75,20 +78,26 @@ SqlString.format = function format(sql, values, stringifyObjects, timeZone) { return sql; } - if (!(values instanceof Array || Array.isArray(values))) { + if (!Array.isArray(values)) { values = [values]; } var chunkIndex = 0; - var placeholdersRegex = /\?\??/g; + var placeholdersRegex = /\?+/g; var result = ''; var valuesIndex = 0; var match; while (valuesIndex < values.length && (match = placeholdersRegex.exec(sql))) { - var value = match[0] === '??' - ? SqlString.escapeId(values[valuesIndex]) - : SqlString.escape(values[valuesIndex], stringifyObjects, timeZone); + var len = match[0].length; + + if (len > 2) { + continue; + } + + var value = len === 2 + ? SqlString.escapeId(values[valuesIndex]) + : SqlString.escape(values[valuesIndex], stringifyObjects, timeZone); result += sql.slice(chunkIndex, match.index) + value; chunkIndex = placeholdersRegex.lastIndex; @@ -155,7 +164,7 @@ SqlString.dateToString = function dateToString(date, timeZone) { }; SqlString.bufferToString = function bufferToString(buffer) { - return "X" + escapeString(buffer.toString('hex')); + return 'X' + escapeString(buffer.toString('hex')); }; SqlString.objectToValues = function objectToValues(object, timeZone) { @@ -174,14 +183,24 @@ SqlString.objectToValues = function objectToValues(object, timeZone) { return sql; }; +SqlString.raw = function raw(sql) { + if (typeof sql !== 'string') { + throw new TypeError('argument sql must be a string'); + } + + return { + toSqlString: function toSqlString() { return sql; } + }; +}; + function escapeString(val) { - var chunkIndex = charsRegex.lastIndex = 0; + var chunkIndex = CHARS_GLOBAL_REGEXP.lastIndex = 0; var escapedVal = ''; var match; - while ((match = charsRegex.exec(val))) { - escapedVal += val.slice(chunkIndex, match.index) + charsMap[match[0]]; - chunkIndex = charsRegex.lastIndex; + while ((match = CHARS_GLOBAL_REGEXP.exec(val))) { + escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]]; + chunkIndex = CHARS_GLOBAL_REGEXP.lastIndex; } if (chunkIndex === 0) { @@ -212,7 +231,7 @@ function convertTimezone(tz) { var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/); if (m) { - return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60; + return (m[1] === '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60; } return false; } diff --git a/package.json b/package.json index 6e1aebe..340e437 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "sqlstring", "description": "Simple SQL escape and format for MySQL", - "version": "2.2.0", + "version": "2.3.3", "contributors": [ "Adri Van Houdt ", "Douglas Christopher Wilson ", "fengmk2 (http://fengmk2.github.com)", - "Nathan Woltman " + "Kevin Jose Martin ", + "Nathan Woltman ", + "Sergej Sintschilin " ], "license": "MIT", "keywords": [ @@ -17,9 +19,11 @@ ], "repository": "mysqljs/sqlstring", "devDependencies": { - "eslint": "3.19.0", - "eslint-plugin-markdown": "1.0.0-beta.6", - "nyc": "10.3.2", + "beautify-benchmark": "0.2.4", + "benchmark": "2.1.4", + "eslint": "8.10.0", + "eslint-plugin-markdown": "2.2.1", + "nyc": "15.1.0", "urun": "0.0.8", "utest": "0.0.8" }, @@ -34,9 +38,10 @@ "node": ">= 0.6" }, "scripts": { - "lint": "eslint --plugin markdown --ext js,md .", + "bench": "node benchmark/index.js", + "lint": "eslint .", "test": "node test/run.js", - "test-ci": "nyc --reporter=text npm test", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", "test-cov": "nyc --reporter=html --reporter=text npm test" } } diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index fe41b9b..580aa4e 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -1,38 +1,51 @@ var assert = require('assert'); var SqlString = require('../../'); var test = require('utest'); +var vm = require('vm'); test('SqlString.escapeId', { 'value is quoted': function() { - assert.equal('`id`', SqlString.escapeId('id')); + assert.equal(SqlString.escapeId('id'), '`id`'); }, 'value can be a number': function() { - assert.equal('`42`', SqlString.escapeId(42)); + assert.equal(SqlString.escapeId(42), '`42`'); + }, + + 'value can be an object': function() { + assert.equal(SqlString.escapeId({}), '`[object Object]`'); + }, + + 'value toString is called': function() { + assert.equal(SqlString.escapeId({ toString: function() { return 'foo'; } }), '`foo`'); + }, + + 'value toString is quoted': function() { + assert.equal(SqlString.escapeId({ toString: function() { return 'f`oo'; } }), '`f``oo`'); }, 'value containing escapes is quoted': function() { - assert.equal('`i``d`', SqlString.escapeId('i`d')); + assert.equal(SqlString.escapeId('i`d'), '`i``d`'); }, 'value containing separator is quoted': function() { - assert.equal('`id1`.`id2`', SqlString.escapeId('id1.id2')); + assert.equal(SqlString.escapeId('id1.id2'), '`id1`.`id2`'); }, 'value containing separator and escapes is quoted': function() { - assert.equal('`id``1`.`i``d2`', SqlString.escapeId('id`1.i`d2')); + assert.equal(SqlString.escapeId('id`1.i`d2'), '`id``1`.`i``d2`'); }, 'value containing separator is fully escaped when forbidQualified': function() { - assert.equal('`id1.id2`', SqlString.escapeId('id1.id2', true)); + assert.equal(SqlString.escapeId('id1.id2', true), '`id1.id2`'); }, 'arrays are turned into lists': function() { - assert.equal(SqlString.escapeId(['a', 'b', 't.c']), "`a`, `b`, `t`.`c`"); + assert.equal(SqlString.escapeId(['a', 'b', 't.c']), '`a`, `b`, `t`.`c`'); }, 'nested arrays are flattened': function() { - assert.equal(SqlString.escapeId(['a', ['b', ['t.c']]]), "`a`, `b`, `t`.`c`"); + assert.equal(SqlString.escapeId(['a', ['b', ['t.c']]]), '`a`, `b`, `t`.`c`'); } }); @@ -54,6 +67,10 @@ test('SqlString.escape', { assert.equal(SqlString.escape(5), '5'); }, + 'raw not escaped': function () { + assert.equal(SqlString.escape(SqlString.raw('NOW()')), 'NOW()'); + }, + 'objects are turned into key value pairs': function() { assert.equal(SqlString.escape({a: 'b', c: 'd'}), "`a` = 'b', `c` = 'd'"); }, @@ -62,22 +79,46 @@ test('SqlString.escape', { assert.equal(SqlString.escape({a: 'b', c: function() {}}), "`a` = 'b'"); }, + 'object values toSqlString is called': function() { + assert.equal(SqlString.escape({id: { toSqlString: function() { return 'LAST_INSERT_ID()'; } }}), '`id` = LAST_INSERT_ID()'); + }, + + 'objects toSqlString is called': function() { + assert.equal(SqlString.escape({ toSqlString: function() { return '@foo_id'; } }), '@foo_id'); + }, + + 'objects toSqlString is not quoted': function() { + assert.equal(SqlString.escape({ toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }), 'CURRENT_TIMESTAMP()'); + }, + 'nested objects are cast to strings': function() { assert.equal(SqlString.escape({a: {nested: true}}), "`a` = '[object Object]'"); }, + 'nested objects use toString': function() { + assert.equal(SqlString.escape({a: { toString: function() { return 'foo'; } }}), "`a` = 'foo'"); + }, + + 'nested objects use toString is quoted': function() { + assert.equal(SqlString.escape({a: { toString: function() { return "f'oo"; } }}), "`a` = 'f\\'oo'"); + }, + 'arrays are turned into lists': function() { assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'"); }, 'nested arrays are turned into grouped lists': function() { - assert.equal(SqlString.escape([[1,2,3], [4,5,6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')"); + assert.equal(SqlString.escape([[1, 2, 3], [4, 5, 6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')"); }, 'nested objects inside arrays are cast to strings': function() { assert.equal(SqlString.escape([1, {nested: true}, 2]), "1, '[object Object]', 2"); }, + 'nested objects inside arrays use toString': function() { + assert.equal(SqlString.escape([1, { toString: function() { return 'foo'; } }, 2]), "1, 'foo', 2"); + }, + 'strings are quoted': function() { assert.equal(SqlString.escape('Super'), "'Super'"); }, @@ -182,6 +223,14 @@ test('SqlString.escape', { assert.strictEqual(string, 'NULL'); }, + 'dates from other isolates are converted': function() { + var expected = '2012-05-07 11:42:03.002'; + var date = vm.runInNewContext('new Date(2012, 4, 7, 11, 42, 3, 2)'); + var string = SqlString.escape(date); + + assert.strictEqual(string, "'" + expected + "'"); + }, + 'buffers are converted to hex': function() { var buffer = new Buffer([0, 1, 254, 255]); var string = SqlString.escape(buffer); @@ -217,6 +266,11 @@ test('SqlString.format', { assert.equal(sql, 'SELECT * FROM `table` WHERE id = 42'); }, + 'triple question marks are ignored': function () { + var sql = SqlString.format('? or ??? and ?', ['foo', 'bar', 'fizz', 'buzz']); + assert.equal(sql, "'foo' or ??? and 'bar'"); + }, + 'extra question marks are left untouched': function() { var sql = SqlString.format('? and ?', ['a']); assert.equal(sql, "'a' and ?"); @@ -248,6 +302,9 @@ test('SqlString.format', { var sql = SqlString.format('?', { toString: function () { return 'hello'; } }, true); assert.equal(sql, "'hello'"); + + var sql = SqlString.format('?', { toSqlString: function () { return '@foo'; } }, true); + assert.equal(sql, '@foo'); }, 'sql is untouched if no values are provided': function () { @@ -260,3 +317,29 @@ test('SqlString.format', { assert.equal(sql, 'SELECT COUNT(*) FROM table'); } }); + +test('SqlString.raw', { + 'creates object': function() { + assert.equal(typeof SqlString.raw('NOW()'), 'object'); + }, + + 'rejects number': function() { + assert.throws(function () { + SqlString.raw(42); + }); + }, + + 'rejects undefined': function() { + assert.throws(function () { + SqlString.raw(); + }); + }, + + 'object has toSqlString': function() { + assert.equal(typeof SqlString.raw('NOW()').toSqlString, 'function'); + }, + + 'toSqlString returns sql as-is': function() { + assert.equal(SqlString.raw("NOW() AS 'current_time'").toSqlString(), "NOW() AS 'current_time'"); + } +});