Completed
Push — master ( 976119...75ac8d )
by greg
17s
created

src/cli/cms/data/sql.js (3 issues)

Check try statements for empty catch clauses.

Best Practice Coding Style Comprehensibility Minor
1
import {parse} from 'node-sqlparser'
2
import {Promise} from 'bluebird'
3
import path from 'path'
4
import _ from 'lodash'
5
import {coreUtils, config, Manager, cmsData} from '../../'
6
7
/**
8
 * take a string and json to escape sql character and convert to sql like syntax
9
 *
10
 * Example: escapeAbeValuesFromStringRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
11
 *
12
 * Return string: select title from ___abe_dot______abe_dot______abe___ where  `abe_meta.template`=`test`
13
 * 
14
 * 
15
 * @param  {String} str      raw abe request sql string
16
 * @param  {Object} jsonPage json object of post
17
 * @return {String}          escaped string
18
 */
19
export function escapeAbeValuesFromStringRequest(str, jsonPage) {
20
  var matchFrom = /from .(.*?) /
21
  var matchVariable = /{{(.*?)}}/
22
23
  var matchFromExec = matchFrom.exec(str)
24
  if (matchFromExec != null && matchFromExec[1] != null) {
25
    var fromMatch
26
    var toReplace = matchFromExec[1]
27
    while ((fromMatch = matchVariable.exec(toReplace))) {
28
      try {
29
        var arFrom = fromMatch[1].split('.')
30
        var value = _.get(jsonPage, arFrom)
31
        if (value != null) {
32
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', value)
33
        } else {
34
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', '')
35
        }
36
      } 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...
37
    }
38
39
    str = str.replace(matchFromExec[1], toReplace)
40
  }
41
42
  var from = /from ([\S\s]+)/.exec(str)
43
44
  var matches = from
45
  if (matches[1]) {
46
    var res = matches[1]
47
    var splitAttr = [
48
      ' where ',
49
      ' order by ',
50
      ' limit ',
51
      ' WHERE ',
52
      ' ORDER BY ',
53
      ' LIMIT '
54
    ]
55
    for (var i = 0; i < splitAttr.length; i++) {
56
      if (res.indexOf(splitAttr[i]) > -1) {
57
        res = res.substring(0, res.indexOf(splitAttr[i]))
58
      }
59
    }
60
    var escapedFrom = res.replace(/\//g, '___abe___')
61
    escapedFrom = escapedFrom.replace(/\./g, '___abe_dot___')
62
    escapedFrom = escapedFrom.replace(/-/g, '___abe_dash___')
63
    str = str.replace(res, escapedFrom)
64
  }
65
66
  str = str.replace(/``/g, "''")
67
68
  return str
69
}
70
71
/**
72
 * analyse and create an object from request sql string
73
 *
74
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
75
 * 
76
 * @param  {String} str      Sql string request
77
 * @param  {Object} jsonPage json of post
78
 * @return {Object}          {type, columns, from, where, string, limit, orderby}
79
 */
80
export function handleSqlRequest(str, jsonPage) {
81
  var req = escapeAbeValuesFromStringRequest(str, jsonPage)
82
  var request = parse(req)
83
  var reconstructSql = ''
84
  // SQL TYPE
85
  var type = ''
86
  if (request.type != null) {
87
    type = request.type
88
  }
89
  reconstructSql += `${type} `
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
  // SQL FROM
103
  var from = []
104
  if (request.from != null) {
105
    Array.prototype.forEach.call(request.from, item => {
106
      from.push(item.table)
107
    })
108
  } else {
109
    from.push('*')
110
  }
111
  reconstructSql += `from ${JSON.stringify(from)} `
112
113
  var where = null
114
  if (request.where != null) {
115
    where = request.where
116
  }
117
118
  var limit = -1
119
  if (request.limit != null) {
120
    limit = request.limit[request.limit.length - 1].value
121
  }
122
123
  var orderby = null
124
  if (request.orderby != null && request.orderby.length > 0) {
125
    orderby = {
126
      column: request.orderby[0].expr.column,
127
      type: request.orderby[0].type
128
    }
129
    reconstructSql += `ORDER BY ${orderby.column} ${orderby.type} `
130
  }
131
132
  return {
133
    type: type,
134
    columns: columns,
135
    from: from,
136
    where: where,
137
    string: reconstructSql,
138
    limit: limit,
139
    orderby: orderby
140
  }
141
}
142
143
/**
144
 * get JSON from abe tag attribute source
145
 *
146
 * {{abe type='data' key='titles' desc='select titles' source='[{"title": "rouge", "id": 1},{"title": "vert", "id": 2},{"title": "blue", "id": 3}]' display="{{title}}"}}
147
 *
148
 * return 
149
 * [{"title": "rouge", "id": 1},{"title": "vert", "id": 2},{"title": "blue", "id": 3}]
150
 * 
151
 * @param  {String} str abe tag
152
 * @return {String}     json string
153
 */
154
export function getDataSource(str) {
155
  var reg = /source=(['|"])(.*?)\1[ |\}]/g
156
  var match = reg.exec(str)
157
  if (match != null) {
158
    return match[2]
159
  }
160
161
  return ''
162
}
163
164
/**
165
 * replaces escaped characters with the right ones
166
 * @param  {String} statement the from clause
167
 * @return {String}           the from sanitized
168
 */
169
export function sanitizeFromStatement(statement) {
170
  var from = ''
171
172
  if (statement != null) {
173
    from = statement[0].replace(/___abe_dot___/g, '.')
174
    from = from.replace(/___abe___/g, '/')
175
    from = from.replace(/___abe_dash___/g, '-')
176
  }
177
178
  return from
179
}
180
181
/**
182
 * calculate the directory to analyze from the from clause
183
 * @param  {String} statement the from clause
184
 * @param  {String} tplPath   the path from the template originator
185
 * @return {string}           the directory to analyze
186
 */
187
export function getFromDirectory(statement, tplPath = '/') {
188
  var pathFromDir = ''
189
190
  if (statement === '' || statement === '*' || statement === '/') {
191
    pathFromDir = Manager.instance.pathData
192
  } else if (statement === './') {
193
    pathFromDir = path.join(Manager.instance.pathData, tplPath)
194
  } else if (statement.indexOf('/') === 0) {
195
    pathFromDir = path.join(Manager.instance.pathData, statement)
196
  } else if (statement.indexOf('/') !== 0) {
197
    pathFromDir = path.join(Manager.instance.pathData, 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.toLowerCase() === 'asc') {
216
        files.sort(cmsData.sort.byDateAsc)
217
      } else if (orderby.type.toLowerCase() === 'desc') {
218
        files.sort(cmsData.sort.byDateDesc)
219
      }
220
    } else {
221
      files.sort(
222
        coreUtils.sort.predicatBy(
223
          orderby.column.toLowerCase(),
224
          orderby.type.toLowerCase() === 'desc' ? -1 : 1
225
        )
226
      )
227
    }
228
  }
229
230
  return files
231
}
232
233
/**
234
 * Keep only published post
235
 *
236
 * keepOnlyPublishedPost([files])
237
 *
238
 * @param  {Array} files      paths
239
 * @return {Array}                files
240
 */
241
export function keepOnlyPublishedPost(files) {
242
  var publishedValue = []
243
  Array.prototype.forEach.call(files, file => {
244
    if (file.publish != null) {
245
      publishedValue.push(file.publish)
246
    }
247
  })
248
249
  return publishedValue
250
}
251
252
/**
253
 * Check array of files have path that match path statement
254
 *
255
 * executeFromClause([array], ['/'], ['/'])
256
 *
257
 * @param  {Array} statement      paths
258
 * @param  {Array} pathFromClause paths
259
 * @return {Array}                files
260
 */
261
export function executeFromClause(files, statement, pathFromClause) {
262
  var from = sanitizeFromStatement(statement)
263
264
  // if the from clause ends with a dot, we won't recurse the directory analyze
265
  if (from.slice(-1) === '.') {
266
    from = from.slice(0, -1)
267
  }
268
269
  var fromDirectory = getFromDirectory(from, pathFromClause)
270
271
  var files_array = files.filter(element => {
272
    if (element.path.indexOf(fromDirectory) > -1) {
273
      return true
274
    }
275
    return false
276
  })
277
278
  return files_array
279
}
280
281
/**
282
 * Execute sql query like to find abe json post that match the query
283
 * 
284
 * @param  {Array} pathQuery of paths
285
 * @param  {String} match     request sql
286
 * @param  {Object} jsonPage  json of post
287
 * @return {Array}           found object that match
288
 */
289
export function execQuery(match, jsonPage) {
290
  var files = keepOnlyPublishedPost(Manager.instance.getList())
291
  var request = handleSqlRequest(
292
    cmsData.regex.getAttr(match, 'source'),
293
    jsonPage
294
  )
295
  var pathQuery =
296
    jsonPage && jsonPage.abe_meta != null && jsonPage.abe_meta.link != null
297
      ? path.dirname(jsonPage.abe_meta.link)
298
      : '/'
299
300
  files = executeFromClause(files, request.from, pathQuery)
301
  files = executeWhereClause(files, request.where, request.columns, jsonPage)
302
  files = executeOrderByClause(files, request.orderby)
303
  files = executeLimitClause(files, request.limit)
304
  return files
305
}
306
307
export function executeQuerySync(match, jsonPage) {
308
  return execQuery(match, jsonPage)
309
}
310
311
export function executeQuery(match, jsonPage) {
312
  var p = new Promise(resolve => {
313
    var res = execQuery(match, jsonPage)
314
    resolve(res)
315
  }).catch(function(e) {
316
    console.error(e)
317
  })
318
319
  return p
320
}
321
322
/**
323
 * check if a given string an url, string json, file url, abe sql request
324
 * 
325
 * get('http://google.com')
326
 * get('{"test":"test"}')
327
 * get('select * from ../')
328
 * get('test')
329
 * 
330
 * @param  {String} str 
331
 * @return {String} url | request | value | file | other
332
 */
333
export function getSourceType(str) {
334
  if(/^(http:\/\/|https:\/\/|:\/\/|\/(.*)\?(.*)[^ ]=)/.test(str)) { 
335
    return 'url'
336
  }
337
338
  if (/select[\S\s]*?from/.test(str)) {
339
    return 'request'
340
  }
341
342
  try {
343
    JSON.parse(str)
344
    return 'value'
345
  } 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...
346
347
  if (/\.json/.test(str)) {
348
    return 'file'
349
  }
350
351
  return 'other'
352
}
353
354
/**
355
 * return array of post that match sql where statement
356
 *
357
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`article`', {})
358
 *
359
 * @param  {Array} files    
360
 * @param  {Object} wheres   clause
361
 * @param  {Int} maxLimit 
362
 * @param  {Array} columns  sql
363
 * @param  {Object} jsonPage json post
364
 * @return {Array}          of files
365
 */
366
export function executeWhereClause(files, wheres, columns, jsonPage) {
367
  if (typeof wheres === 'undefined' || wheres === null) return files
368
  var res = []
369
  var json = {}
370
  var jsonValues = {}
371
372
  for (let file of files) {
373
    if (wheres != null) {
374
      if (!recurseWhere(wheres, file, jsonPage)) {
375
        json = JSON.parse(JSON.stringify(file))
376
        jsonValues = {}
377
378
        if (columns != null && columns.length > 0 && columns[0] !== '*') {
379
          Array.prototype.forEach.call(columns, column => {
380
            if (json[column] != null) {
381
              jsonValues[column] = json[column]
382
            }
383
          })
384
          jsonValues['abe_meta'] = json['abe_meta']
385
        } else {
386
          jsonValues = json
387
        }
388
389
        res.push(jsonValues)
390
      }
391
    }
392
  }
393
394
  return res
395
}
396
397
/**
398
 * return array of post in the limit
399
 *
400
 * Example: executeLimitClause({}, 2)
401
 *
402
 * @param  {Array} files
403
 * @param  {Int} maxLimit 
404
 * @return {Array} of files
405
 */
406
export function executeLimitClause(files, maxLimit) {
407
  if (files.length > maxLimit && maxLimit > 0) {
408
    files.splice(maxLimit, files.length - maxLimit)
409
  }
410
411
  return files
412
}
413
414
/**
415
 * Compare where left and where right clause
416
 * 
417
 * @param  {Object} where           clause
418
 * @param  {Object} jsonDoc         json of current post
419
 * @param  {Object} jsonOriginalDoc json of post to compare
420
 * @return {Object}                 {left: value, right: value}
421
 */
422
export function getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc) {
423
  var regexIsVariable = /^{{(.*)}}$/
424
  var value = null
425
  var compare = null
426
427
  try {
428
    var variableLeft = where.left.column
429
    var whereLeftColumn = where.left.column
430
    var checkIfLeftIsAVariable = regexIsVariable.exec(variableLeft)
431
    if (checkIfLeftIsAVariable != null && checkIfLeftIsAVariable.length > 0) {
432
      variableLeft = checkIfLeftIsAVariable[1]
433
    }
434
    var arVariableLeft = variableLeft.split('.')
435
    value = _.get(jsonDoc, arVariableLeft)
436
  } catch (e) {
437
    // console.log('e', e)
438
  }
439
440
  if (where.operator === 'IN' || where.operator === 'NOT IN') {
441
    if (value == null) {
442
      // is the form of variableLeft something like elt.attribute or elt[].attribute?
443
      // so maybe is elt an array ?
444
      let arValues = variableLeft.split('.')
445
      let key = arValues[0]
446
      if (key.slice(-2) == '[]') {
447
        key = key.slice(0, -2)
448
      }
449
      let records = _.get(jsonDoc, key)
450
      whereLeftColumn = arValues[1]
451
      value = []
452
      // if yes, value is then an array of values
453
      if (records != null) {
454
        Array.prototype.forEach.call(records, record => {
455
          try {
456
            let val = record[arValues[1]]
457
            value.push(val)
458
          } catch (e) {
459
            console.log(e.stack)
460
          }
461
        })
462
      }
463
    }
464
    compare = []
465
    Array.prototype.forEach.call(where.right.value, right => {
466
      var matchRightVariable = regexIsVariable.exec(right.column)
467
      if (matchRightVariable != null && matchRightVariable.length > 0) {
468
        try {
469
          var jsonOriginalValues = _.get(jsonOriginalDoc, matchRightVariable[1])
470
          if (
471
            Object.prototype.toString.call(jsonOriginalValues) ===
472
            '[object Array]'
473
          ) {
474
            Array.prototype.forEach.call(
475
              jsonOriginalValues,
476
              jsonOriginalValue => {
477
                compare.push(_.get(jsonOriginalValue, whereLeftColumn))
478
              }
479
            )
480
          } else {
481
            compare.push(jsonOriginalValues)
482
          }
483
        } 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...
484
      } else {
485
        compare.push(right.column)
486
      }
487
    })
488
  } else {
489
    if (where.right.column != null) {
490
      compare = where.right.column
491
    } else if (where.right.value != null) {
492
      compare = where.right.value
493
    }
494
495
    var matchRightVariable = regexIsVariable.exec(compare)
496
497
    if (matchRightVariable != null && matchRightVariable.length > 0) {
498
      try {
499
        var arVariableRight = matchRightVariable[1].split('.')
500
        var shouldCompare = _.get(jsonOriginalDoc, arVariableRight)
501
        if (shouldCompare != null) {
502
          compare = shouldCompare
503
        } else {
504
          compare = null
505
        }
506
      } catch (e) {
507
        compare = null
508
      }
509
    }
510
  }
511
512
  return {
513
    left: value,
514
    right: compare
515
  }
516
}
517
518
export function isInStatementCorrect(values, isCorrect) {
519
  let hasPassedTest = false
520
  if (Object.prototype.toString.call(values.left) === '[object Array]') {
521
    Array.prototype.forEach.call(values.left, left => {
522
      Array.prototype.forEach.call(values.right, right => {
523
        if (left != null && left === right) {
524
          hasPassedTest = true
525
        }
526
      })
527
    })
528
  } else {
529
    Array.prototype.forEach.call(values.right, right => {
530
      if (values.left === right) {
531
        hasPassedTest = true
532
      }
533
    })
534
  }
535
536
  if (hasPassedTest) isCorrect = !isCorrect
537
538
  return isCorrect
539
}
540
541
/**
542
 * Check where.left value that match where operator (=, !=, >, >=, <, <=, LIKE, NOT LIKE, AND, OR, IN, NOT IN)
543
 * if operator AND or OR
544
 * Recurse on where.left and where.right sql clause
545
 *
546
 * 
547
 * @param  {Object} where           clause
548
 * @param  {Object} jsonDoc         json of current post
549
 * @param  {Object} jsonOriginalDoc json of post to compare
550
 * @return {Boolean}                 true if not matching | false if matching
551
 */
552
export function recurseWhere(where, jsonDoc, jsonOriginalDoc) {
553
  var isNotLeftCorrect = false
554
  var isNotRightCorrect = false
555
  var isNotCorrect = false
556
  var values
557
558
  switch (where.operator) {
559
    case '=':
560
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
561
      isNotCorrect = !(values.left === values.right)
562
      break
563
    case '!=':
564
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
565
      isNotCorrect = !(values.left !== values.right)
566
      break
567
    case '>':
568
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
569
      isNotCorrect = !(values.left > values.right)
570
      break
571
    case '>=':
572
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
573
      isNotCorrect = !(values.left >= values.right)
574
      break
575
    case '<':
576
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
577
      isNotCorrect = !(values.left < values.right)
578
      break
579
    case '<=':
580
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
581
      isNotCorrect = !(values.left <= values.right)
582
      break
583
    case 'LIKE':
584
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
585
      isNotCorrect = !(values.left && values.left.indexOf(values.right) > -1)
586
      break
587
    case 'NOT LIKE':
588
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
589
      isNotCorrect = !(values.left && values.left.indexOf(values.right) === -1)
590
      break
591
    case 'AND':
592
      isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
593
      isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
594
      isNotCorrect = isNotLeftCorrect || isNotRightCorrect ? true : false
595
      break
596
    case 'OR':
597
      isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
598
      isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
599
      isNotCorrect = isNotLeftCorrect && isNotRightCorrect ? true : false
600
      break
601
    case 'IN':
602
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
603
      isNotCorrect = isInStatementCorrect(values, true)
604
      break
605
    case 'NOT IN':
606
      values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
607
      isNotCorrect = isInStatementCorrect(values, false)
608
      break
609
  }
610
611
  return isNotCorrect
612
}
613