Completed
Push — master ( 7326eb...48936b )
by greg
02:04
created

sql.js ➔ ... ➔ ???   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 5
rs 9.4285
1
import {parse} from 'node-sqlparser'
2
import {Promise} from 'bluebird'
3
import path from 'path'
4
import {
5
  coreUtils,
6
  config,
7
  Manager,
8
  cmsData
9
} from '../../'
10
11
/**
12
 * take a string and json to escape sql character and convert to sql like syntax
13
 *
14
 * Example: escapeAbeValuesFromStringRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
15
 *
16
 * Return string: select title from ___abe_dot______abe_dot______abe___ where  `abe_meta.template`=`test`
17
 * 
18
 * 
19
 * @param  {String} str      raw abe request sql string
20
 * @param  {Object} jsonPage json object of post
21
 * @return {String}          escaped string
22
 */
23
export function escapeAbeValuesFromStringRequest(str, jsonPage) {
0 ignored issues
show
Unused Code introduced by
The parameter jsonPage is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
24
  var matchFrom = /from .(.*?) /
25
  var matchVariable = /{{([a-zA-Z]*)}}/
26
27
  var matchFromExec = matchFrom.exec(str)
28
  if(matchFromExec != null && matchFromExec[1] != null) {
29
30
    var fromMatch
31
    var toReplace = matchFromExec[1]
32
    while (fromMatch = matchVariable.exec(toReplace)) {
33
      try {
34
        var value = eval('jsonPage.' + fromMatch[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
35
        if(value != null) {
36
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', value)
37
        }else {
38
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', '')
39
        }
40
      }catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
41
      }
42
    }
43
44
    str = str.replace(matchFromExec[1], toReplace)
45
  }
46
47
  var from = /from ([\S\s]+)/.exec(str)
48
49
  var matches = from
50
  if(matches[1]) {
51
    var res = matches[1]
52
    var splitAttr = [' where ', ' order by ', ' limit ', ' WHERE ', ' ORDER BY ', ' LIMIT ']
53
    for(var i = 0; i < splitAttr.length; i++) {
54
      if(res.indexOf(splitAttr[i]) > -1) {
55
        res = res.substring(0, res.indexOf(splitAttr[i]))
56
      }
57
    }
58
    var escapedFrom = res.replace(/\//g, '___abe___')
59
    escapedFrom = escapedFrom.replace(/\./g, '___abe_dot___')
60
    escapedFrom = escapedFrom.replace(/-/g, '___abe_dash___')
61
    str = str.replace(res, escapedFrom)
62
  }
63
64
  str = str.replace(/``/g, '\'\'')
65
66
  return str
67
}
68
69
/**
70
 * analyse and create an object from request sql string
71
 *
72
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
73
 * 
74
 * @param  {String} str      Sql string request
75
 * @param  {Object} jsonPage json of post
76
 * @return {Object}          {type, columns, from, where, string, limit, orderby}
77
 */
78
export function handleSqlRequest(str, jsonPage) {
79
  var req = escapeAbeValuesFromStringRequest(str, jsonPage)
80
  var request = parse(req)
81
  var reconstructSql = ''
82
83
  // SQL TYPE
84
  var type = ''
85
  if(request.type != null) {
86
    type = request.type
87
  }
88
  reconstructSql += `${type} `
89
90
  // SQL COLUMNS
91
  var columns = []
92
  if(request.columns != null) {
93
    if(request.columns === '*') {
94
      columns.push('*')
95
    }else {
96
      Array.prototype.forEach.call(request.columns, (item) => {
97
        columns.push(item.expr.column)
98
      })
99
    }
100
  }
101
  reconstructSql += `${JSON.stringify(columns)} `
102
103
  // SQL FROM
104
  var from = []
105
  if(request.from != null) {
106
107
    Array.prototype.forEach.call(request.from, (item) => {
108
      from.push(item.table)
109
    })
110
  }else {
111
    from.push('*')
112
  }
113
  reconstructSql += `from ${JSON.stringify(from)} `
114
115
  var where = null
116
  if(request.where != null) {
117
    where = request.where
118
  }
119
120
  var limit = -1
121
  if(request.limit != null) {
122
    limit = request.limit[request.limit.length - 1].value
123
  }
124
125
  var orderby = null
126
  if(request.orderby != null && request.orderby.length > 0) {
127
    orderby = {
128
      column: request.orderby[0].expr.column,
129
      type: request.orderby[0].type
130
    }
131
    reconstructSql += `ORDER BY ${orderby.column} ${orderby.type} `
132
  }
133
134
  return {
135
    type: type,
136
    columns: columns,
137
    from: from,
138
    where: where,
139
    string: reconstructSql,
140
    limit: limit,
141
    orderby: orderby
142
  }
143
}
144
145
export function getDataSource(str) {
146
  var res = str.substring(str.indexOf('source=') + 8, str.length)
147
148
  var reg = /([^'"]*=[\s\S]*?}})/g
149
  var matches = res.match(reg)
150
  if(matches != null) {
151
    Array.prototype.forEach.call(matches, (match) => {
152
      res = res.replace(match, '')
153
    })
154
  }else {
155
    res = res.replace('}}', '')
156
  }
157
158
  return res.substring(0, res.length-1)
159
}
160
161
/**
162
 * replaces escaped characters with the right ones
163
 * @param  {String} statement the from clause
164
 * @return {String}           the from sanitized
165
 */
166
export function sanitizeFromStatement(statement){
167
  var from = ''
168
169
  if(statement != null) {
170
    from = statement[0].replace(/___abe_dot___/g, '.')
171
    from = from.replace(/___abe___/g, '/')
172
    from = from.replace(/___abe_dash___/g, '-')
173
  }
174
175
  return from
176
}
177
178
/**
179
 * calculate the directory to analyze from the from clause
180
 * @param  {String} statement the from clause
181
 * @param  {String} tplPath   the path from the template originator
182
 * @return {string}           the directory to analyze
183
 */
184
export function getFromDirectory(statement, tplPath){
185
  var pathFromDir = ''
186
  if(!tplPath){
187
    tplPath = '/'
188
  }
189
190
  if(statement === '' || statement === '*' || statement === '/') {
191
    pathFromDir = path.join(config.root, config.data.url)
192
  }else if(statement === './') {
193
    pathFromDir = path.join(config.root, config.data.url, tplPath)
194
  }else if(statement.indexOf('/') === 0) {
195
    pathFromDir = path.join(config.root, config.data.url, statement)
196
  }else if(statement.indexOf('/') !== 0) {
197
    pathFromDir = path.join(config.root, config.data.url, tplPath, statement)
198
  }
199
200
  return pathFromDir
201
}
202
203
/**
204
 * sort array of files from where clause
205
 *
206
 * @param  {Array} files
207
 * @param  {Object} orderby {orderby: {column: 'date'}} | {orderby: {column: 'random', type: 'ASC'}}
208
 * @return {Array}         sorted array
209
 */
210
export function executeOrderByClause(files, orderby){
211
  if(orderby != null) {
212
    if(orderby.column.toLowerCase() === 'random') {
213
      files = coreUtils.sort.shuffle(files)
214
    }else if(orderby.column.toLowerCase() === 'date') {
215
      if(orderby.type === 'ASC') {
216
        files.sort(cmsData.sort.byDateAsc)
217
      }else if(orderby.type === 'DESC') {
218
        files.sort(cmsData.sort.byDateDesc)
219
      }
220
    }
221
  }
222
223
  return files
224
}
225
226
/**
227
 * Keep only published post
228
 *
229
 * keepOnlyPublishedPost([files])
230
 *
231
 * @param  {Array} files      paths
232
 * @return {Array}                files
233
 */
234
export function keepOnlyPublishedPost(files){
235
  var publishedValue = []
236
  Array.prototype.forEach.call(files, (file) => {
237
    if (file.publish != null) {
238
      publishedValue.push(file.publish)
239
    }
240
  })
241
242
  return publishedValue
243
}
244
245
/**
246
 * Check array of files have path that match path statement
247
 *
248
 * executeFromClause([array], ['/'], ['/'])
249
 *
250
 * @param  {Array} statement      paths
251
 * @param  {Array} pathFromClause paths
252
 * @return {Array}                files
253
 */
254
export function executeFromClause(files, statement, pathFromClause){
255
  var from = sanitizeFromStatement(statement)
256
257
  // if the from clause ends with a dot, we won't recurse the directory analyze
258
  if(from.slice(-1) === '.'){
259
    from = from.slice(0, -1)
260
  }
261
  
262
  var fromDirectory = getFromDirectory(from, pathFromClause)
263
264
  var files_array = files.filter((element) => {
265
    if (element.path.indexOf(fromDirectory) > -1) {
266
      return true
267
    }
268
    return false
269
  })
270
271
  return files_array
272
}
273
274
/**
275
 * Execute sql query like to find abe json post that match the query
276
 * 
277
 * @param  {Array} pathQuery of paths
278
 * @param  {String} match     request sql
279
 * @param  {Object} jsonPage  json of post
280
 * @return {Array}           found object that match
281
 */
282
export function execQuery(pathQuery, match, jsonPage) {
283
  var files = keepOnlyPublishedPost(Manager.instance.getList())
284
  var request = handleSqlRequest(cmsData.regex.getAttr(match, 'source'), jsonPage)
285
286
  files = executeFromClause(files, request.from, pathQuery)
287
  files = executeWhereClause(files, request.where, request.limit, request.columns, jsonPage)
288
  files = executeOrderByClause(files, request.orderby)
289
  return files
290
}
291
292
export function executeQuerySync(pathQuerySync, match, jsonPage) {
293
  return execQuery(pathQuerySync, match, jsonPage)
294
}
295
296
export function executeQuery(pathexecuteQuery, match, jsonPage) {
297
  var p = new Promise((resolve) => {
298
    var res = execQuery(pathexecuteQuery, match, jsonPage)
299
    resolve(res)
300
  }).catch(function(e) {
301
    console.error(e)
302
  })
303
304
  return p
305
}
306
307
/**
308
 * check if a given string an url, string json, file url, abe sql request
309
 * 
310
 * get('http://google.com')
311
 * get('{"test":"test"}')
312
 * get('select * from ../')
313
 * get('test')
314
 * 
315
 * @param  {String} str 
316
 * @return {String} url | request | value | file | other
317
 */
318
export function getSourceType(str) {
319
  if(/http:\/\/|https:\/\//.test(str)) {
320
    return 'url'
321
  }
322
323
  if(/select[\S\s]*?from/.test(str)) {
324
    return 'request'
325
  }
326
327
  try {
328
    JSON.parse(str)
329
    return 'value'
330
  }catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
331
332
  }
333
334
  if(/\.json/.test(str)) {
335
    return 'file'
336
  }
337
338
  return 'other'
339
}
340
341
/**
342
 * return array of post that match sql where statement
343
 *
344
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`article`', {})
345
 *
346
 * @param  {Array} files    
347
 * @param  {Object} wheres   clause
348
 * @param  {Int} maxLimit 
349
 * @param  {Array} columns  sql
350
 * @param  {Object} jsonPage json post
351
 * @return {Array}          of files
352
 */
353
export function executeWhereClause(files, wheres, maxLimit, columns, jsonPage){
354
  if(typeof wheres === 'undefined' || wheres === null) return files
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
355
  var res = []
356
  var limit = 0
357
  var json = {}
358
  var jsonValues = {}
359
360
  for(let file of files) {
361
    if(limit < maxLimit || maxLimit === -1) {
362
      if(wheres != null) {
363
        if(!recurseWhere(wheres, file, jsonPage)) {
364
          json = JSON.parse(JSON.stringify(file))
365
          jsonValues = {}
366
367
          if(columns != null && columns.length > 0 && columns[0] !== '*') {
368
            Array.prototype.forEach.call(columns, (column) => {
369
              if(json[column] != null) {
0 ignored issues
show
Bug introduced by
The variable json is changed as part of the for-each loop for example by JSON.parse(JSON.stringify(file)) on line 364. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
370
                jsonValues[column] = json[column]
0 ignored issues
show
Bug introduced by
The variable jsonValues is changed as part of the for-each loop for example by {} on line 365. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
371
              }
372
            })
373
            jsonValues['abe_meta'] = json['abe_meta']
374
          }else {
375
            jsonValues = json
376
          }
377
378
          res.push(jsonValues)
379
          limit++
380
        }
381
      }
382
    } else {
383
      break
384
    }
385
  }
386
387
  return res
388
}
389
390
/**
391
 * Compare where left and where right clause
392
 * 
393
 * @param  {Object} where           clause
394
 * @param  {Object} jsonDoc         json of current post
395
 * @param  {Object} jsonOriginalDoc json of post to compare
396
 * @return {Object}                 {left: value, right: value}
397
 */
398
export function getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc) {
0 ignored issues
show
Unused Code introduced by
The parameter jsonOriginalDoc is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter jsonDoc is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
399
  var regexIsVariable = /^{{(.*)}}$/
400
  var value = null
401
  var compare = null
402
403
  try {
404
    var variableLeft = where.left.column
405
    var whereLeftColumn = where.left.column
406
    var checkIfLeftIsAVariable = regexIsVariable.exec(variableLeft)
407
    if(checkIfLeftIsAVariable != null && checkIfLeftIsAVariable.length > 0) {
408
      variableLeft = checkIfLeftIsAVariable[1]
409
    }
410
    value = eval('jsonDoc.' + variableLeft)
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
411
  }catch(e) {
412
    // console.log('e', e)
413
  }
414
  
415
  if(where.operator === 'IN' || where.operator === 'NOT IN') {
416
    if(value == null) {
417
      // is the form of variableLeft something like elt.attribute or elt[].attribute?
418
      // so maybe is elt an array ?
419
      let arValues = variableLeft.split('.')
420
      let key = arValues[0]
421
      if(key.slice(-2) == '[]'){
422
        key = key.slice(0, -2)
423
      }
424
      let records = eval('jsonDoc.' + key)
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
425
      whereLeftColumn = arValues[1]
426
      value = []
427
      // if yes, value is then an array of values
428
      if(records != null) {
429
        Array.prototype.forEach.call(records, (record) => {
430
          try {
431
            let val = record[arValues[1]]
432
            value.push(val)
433
          } catch (e) {
434
            console.log(e.stack)
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
435
          }
436
        })
437
      }
438
    }
439
    compare = []
440
    Array.prototype.forEach.call(where.right.value, (right) => {
441
      var matchRightVariable = regexIsVariable.exec(right.column)
442
      if(matchRightVariable != null && matchRightVariable.length > 0) {
443
        try {
444
          var jsonOriginalValues = eval('jsonOriginalDoc.' + matchRightVariable[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
445
          Array.prototype.forEach.call(jsonOriginalValues, (jsonOriginalValue) => {
0 ignored issues
show
Unused Code introduced by
The parameter jsonOriginalValue is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
446
            compare.push(eval('jsonOriginalValue.' + whereLeftColumn))
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
Bug introduced by
The variable whereLeftColumn does not seem to be initialized in case var variableLeft = where.left.column on line 404 throws an error. Are you sure this can never be the case?
Loading history...
447
          })
448
        }catch(e) {}
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
449
      }
450
      else{
451
        compare.push(right.column)
452
      }
453
    })
454
  } else {
455
    if(where.right.column != null) {
456
      compare = where.right.column
457
    } else if(where.right.value != null) {
458
      compare = where.right.value
459
    }
460
461
    var matchRightVariable = regexIsVariable.exec(compare)
462
463
    if(matchRightVariable != null && matchRightVariable.length > 0) {
464
      try {
465
        var shouldCompare = eval('jsonOriginalDoc.' + matchRightVariable[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
466
        if(shouldCompare != null) {
467
          compare = shouldCompare
468
        }else {
469
          compare = null
470
        }
471
      }catch(e) {
472
        compare = null
473
      }
474
    }
475
  }
476
477
  return {
478
    left: value,
479
    right: compare
480
  }
481
}
482
483
export function isInStatementCorrect(values, isCorrect){
484
  if( Object.prototype.toString.call(values.left) === '[object Array]' ) {
485
    Array.prototype.forEach.call(values.left, (left) => {
486
      Array.prototype.forEach.call(values.right, (right) => {
487
        if(left != null && left === right) {
488
          isCorrect = !isCorrect
489
        }
490
      })
491
    })
492
  } else {
493
    Array.prototype.forEach.call(values.right, (right) => {
494
      if(values.left === right) {
495
        isCorrect = !isCorrect
496
      }
497
    })
498
  }
499
500
  return isCorrect
501
}
502
503
/**
504
 * Check where.left value that match where operator (=, !=, >, >=, <, <=, LIKE, NOT LIKE, AND, OR, IN, NOT IN)
505
 * if operator AND or OR
506
 * Recurse on where.left and where.right sql clause
507
 *
508
 * 
509
 * @param  {Object} where           clause
510
 * @param  {Object} jsonDoc         json of current post
511
 * @param  {Object} jsonOriginalDoc json of post to compare
512
 * @return {Boolean}                 true if not matching | false if matching
513
 */
514
export function recurseWhere(where, jsonDoc, jsonOriginalDoc) {
515
  var isNotLeftCorrect = false
0 ignored issues
show
Unused Code introduced by
The assignment to variable isNotLeftCorrect seems to be never used. Consider removing it.
Loading history...
516
  var isNotRightCorrect = false
0 ignored issues
show
Unused Code introduced by
The assignment to variable isNotRightCorrect seems to be never used. Consider removing it.
Loading history...
517
  var isNotCorrect = false
518
  var values
519
520
  switch(where.operator) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
521
  case '=':
522
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
523
    isNotCorrect = !(values.left === values.right)
524
    break
525
  case '!=':
526
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
527
    isNotCorrect = !(values.left !== values.right)
528
    break
529
  case '>':
530
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
531
    isNotCorrect = !(values.left > values.right)
532
    break
533
  case '>=':
534
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
535
    isNotCorrect = !(values.left >= values.right)
536
    break
537
  case '<':
538
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
539
    isNotCorrect = !(values.left < values.right)
540
    break
541
  case '<=':
542
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
543
    isNotCorrect = !(values.left <= values.right)
544
    break
545
  case 'LIKE':
546
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
547
    isNotCorrect = !(values.left && values.left.indexOf(values.right) > -1)
548
    break
549
  case 'NOT LIKE':
550
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
551
    isNotCorrect = !(values.left && values.left.indexOf(values.right) === -1)
552
    break
553
  case 'AND':
554
    isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
555
    isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
556
    isNotCorrect = (isNotLeftCorrect || isNotRightCorrect) ? true : false
557
    break
558
  case 'OR':
559
    isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
560
    isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
561
    isNotCorrect = (isNotLeftCorrect && isNotRightCorrect) ? true : false
562
    break
563
  case 'IN':
564
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
565
    isNotCorrect = isInStatementCorrect(values, true)
566
    break
567
  case 'NOT IN':
568
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
569
    isNotCorrect = isInStatementCorrect(values, false)
570
    break
571
  }
572
573
  return isNotCorrect
574
}