View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.model.accessible;
12  
13  import com.jsql.model.InjectionModel;
14  import com.jsql.model.bean.database.*;
15  import com.jsql.model.bean.util.Interaction;
16  import com.jsql.model.bean.util.Request;
17  import com.jsql.model.exception.AbstractSlidingException;
18  import com.jsql.model.exception.InjectionFailureException;
19  import com.jsql.model.exception.JSqlException;
20  import com.jsql.model.suspendable.SuspendableGetRows;
21  import com.jsql.util.I18nUtil;
22  import com.jsql.util.LogLevelUtil;
23  import org.apache.commons.lang3.StringUtils;
24  import org.apache.logging.log4j.LogManager;
25  import org.apache.logging.log4j.Logger;
26  
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.regex.Pattern;
30  
31  /**
32   * Database resource object to read name of databases, tables, columns and values
33   * using most suited injection strategy.
34   */
35  public class DataAccess {
36      
37      /**
38       * Log4j logger sent to view.
39       */
40      private static final Logger LOGGER = LogManager.getRootLogger();
41      
42      /**
43       * Regex characters marking the end of the result of an injection.
44       * Process stops when this schema is encountered:
45       * <pre>SQLix01x03x03x07
46       */
47      public static final String TRAIL_RGX = "\\x01\\x03\\x03\\x07";
48      
49      /**
50       * Regex character used between each table cells.
51       * Expected schema of multiple table cells :
52       * <pre>
53       * x04[table cell]x05[number of occurrences]x04x06x04[table cell]x05[number of occurrences]x04
54       */
55      public static final String SEPARATOR_CELL_RGX = "\\x06";
56      
57      /**
58       * Regex character used between the table cell and the number of occurrence of the cell text.
59       * Expected schema of a table cell data is
60       * <pre>x04[table cell]x05[number of occurrences]x04
61       */
62      public static final String SEPARATOR_QTE_RGX = "\\x05";
63  
64      /**
65       * Regex character enclosing a table cell returned by injection.
66       * It allows to detect the correct end of a table cell data during parsing.
67       * Expected schema of a table cell data is
68       * <pre>x04[table cell]x05[number of occurrences]x04
69       */
70      public static final String ENCLOSE_VALUE_RGX = "\\x04";
71      
72      public static final String LEAD = "SqLi";
73      public static final String SHELL_LEAD = "${shell.lead}";
74      public static final String TRAIL = "iLQS";
75      public static final String SHELL_TRAIL = "${shell.trail}";
76      
77      /**
78       * Regex keywords corresponding to multiline and case-insensitive match.
79       */
80      public static final String MODE = "(?si)";
81      
82      /**
83       * Regex schema describing a table cell with firstly the cell content and secondly the number of occurrences
84       * of the cell text, separated by the reserved character x05 in hexadecimal.
85       * The range of characters from x01 to x1F are not printable ASCII characters used to parse the data and exclude
86       * printable characters during parsing.
87       * Expected schema of a table cell data is
88       * <pre>x04[table cell]x05[number of occurrences]x04
89       */
90      public static final String CELL_TABLE = "([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*)"+ DataAccess.SEPARATOR_QTE_RGX +"([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*)(\\x08)?";
91  
92      private final InjectionModel injectionModel;
93      
94      public DataAccess(InjectionModel injectionModel) {
95          this.injectionModel = injectionModel;
96      }
97      
98      /**
99       * Get general database information.<br>
100      * => version{%}database{%}user{%}CURRENT_USER
101      */
102     public void getDatabaseInfos() {
103         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_FETCHING_INFORMATIONS"));
104         
105         var sourcePage = new String[]{ StringUtils.EMPTY };
106 
107         var resultToParse = StringUtils.EMPTY;
108         try {
109             resultToParse = new SuspendableGetRows(this.injectionModel).run(
110                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlInfos(),
111                 sourcePage,
112                 false,
113                 0,
114                 MockElement.MOCK,
115                 "metadata"
116             );
117         } catch (AbstractSlidingException e) {
118             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
119         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
120             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
121         }
122 
123         if (StringUtils.isEmpty(resultToParse)) {
124             this.injectionModel.sendResponseFromSite("Incorrect metadata", sourcePage[0].trim());
125         }
126         
127         try {
128             String versionDatabase = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[0].replaceAll("\\s+", StringUtils.SPACE);
129             String nameDatabase = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[1];
130             String username = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[2];
131 
132             var infos = String.format(
133                 "Database [%s] on %s [%s] for user [%s]",
134                 nameDatabase,
135                 this.injectionModel.getMediatorVendor().getVendor(),
136                 versionDatabase,
137                 username
138             );
139             LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, infos);
140         } catch (ArrayIndexOutOfBoundsException e) {
141             LOGGER.log(
142                 LogLevelUtil.CONSOLE_ERROR,
143                 String.format("%s: %s", I18nUtil.valueByKey("LOG_DB_METADATA_INCORRECT"), resultToParse)
144             );
145             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, I18nUtil.valueByKey("LOG_DB_METADATA_WARN"));
146         }
147     }
148 
149     /**
150      * Get database names and table counts and send them to the view.<br>
151      * Use readable text (not hexa) and parse this pattern:<br>
152      * => hh[database name 1]jj[table count]hhgghh[database name 2]jj[table count]hhggh...hi<br>
153      * Data window can be cut before the end of the request but the process helps to obtain
154      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
155      * @return list of databases found
156      * @throws JSqlException when injection failure or stopped by user
157      */
158     public List<Database> listDatabases() throws JSqlException {
159         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_FETCHING_DATABASES"));
160         List<Database> databases = new ArrayList<>();
161         String resultToParse = StringUtils.EMPTY;
162         
163         try {
164             var sourcePage = new String[]{ StringUtils.EMPTY };
165             resultToParse = new SuspendableGetRows(this.injectionModel).run(
166                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlDatabases(),
167                 sourcePage,
168                 true,
169                 0,
170                 MockElement.MOCK,
171                 "databases"
172             );
173         } catch (AbstractSlidingException e) {
174             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
175         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
176             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
177         }
178 
179         // Parse all data we have retrieved
180         var regexSearch = Pattern.compile(
181                 DataAccess.MODE
182                 + DataAccess.ENCLOSE_VALUE_RGX
183                 + DataAccess.CELL_TABLE
184                 + DataAccess.ENCLOSE_VALUE_RGX
185             )
186             .matcher(resultToParse);
187         if (!regexSearch.find()) {
188             throw new InjectionFailureException("No match while injecting databases");
189         }
190         
191         regexSearch.reset();
192         // Build an array of Database objects from the data we have parsed
193         while (regexSearch.find()) {
194             String databaseName = regexSearch.group(1);
195             String tableCount = regexSearch.group(2);
196 
197             var newDatabase = new Database(databaseName, tableCount);
198             databases.add(newDatabase);
199         }
200 
201         var request = new Request();
202         request.setMessage(Interaction.ADD_DATABASES);
203         request.setParameters(databases);
204         this.injectionModel.sendToViews(request);
205         return databases;
206     }
207 
208     /**
209      * Get tables name and row count and send them to the view.<br>
210      * Use readable text (not hexa) and parse this pattern:<br>
211      * => hh[table name 1]jj[rows count]hhgghh[table name 2]jj[rows count]hhggh...hi<br>
212      * Data window can be cut before the end of the request but the process helps to obtain
213      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
214      * @param database which contains tables to find
215      * @return list of tables found
216      * @throws JSqlException when injection failure or stopped by user
217      */
218     public List<Table> listTables(Database database) throws JSqlException {
219         // Reset stoppedByUser if list of Databases is partial
220         // and some Tables are still reachable
221         this.injectionModel.setIsStoppedByUser(false);
222         
223         // Inform the view that database has just been used
224         var requestStartProgress = new Request();
225         requestStartProgress.setMessage(Interaction.START_PROGRESS);
226         requestStartProgress.setParameters(database);
227         this.injectionModel.sendToViews(requestStartProgress);
228 
229         var tableCount = Integer.toString(database.getChildCount());
230         
231         String resultToParse = StringUtils.EMPTY;
232         try {
233             var pageSource = new String[]{ StringUtils.EMPTY };
234             resultToParse = new SuspendableGetRows(this.injectionModel).run(
235                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlTables(database),
236                 pageSource,
237                 true,
238                 Integer.parseInt(tableCount),
239                 database,
240                 "tables"
241             );
242         } catch (AbstractSlidingException e) {
243             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
244         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
245             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
246         }
247 
248         // Parse all the data we have retrieved
249         var regexSearch = Pattern.compile(
250                 DataAccess.MODE
251                 + DataAccess.ENCLOSE_VALUE_RGX
252                 + DataAccess.CELL_TABLE
253                 + DataAccess.ENCLOSE_VALUE_RGX
254             )
255             .matcher(resultToParse);
256         
257         var requestEndProgress = new Request();
258         requestEndProgress.setMessage(Interaction.END_PROGRESS);
259         requestEndProgress.setParameters(database);
260         this.injectionModel.sendToViews(requestEndProgress);
261         
262         if (!regexSearch.find()) {
263             throw new InjectionFailureException("No match while injecting tables");
264         }
265         regexSearch.reset();
266         
267         List<Table> tables = new ArrayList<>();
268         
269         // Build an array of Table objects from the data we have parsed
270         while (regexSearch.find()) {
271             String tableName = regexSearch.group(1);
272             String rowCount = regexSearch.group(2);
273             
274             var newTable = new Table(tableName, rowCount, database);
275             tables.add(newTable);
276         }
277         
278         var requestAddTables = new Request();
279         requestAddTables.setMessage(Interaction.ADD_TABLES);
280         requestAddTables.setParameters(tables);
281         this.injectionModel.sendToViews(requestAddTables);
282         return tables;
283     }
284 
285     /**
286      * Get column names and send them to the view.<br>
287      * Use readable text (not hexa) and parse this pattern with 2nd member forced to 31 (1 in ascii):<br>
288      * => hh[column name 1]jj[31]hhgghh[column name 2]jj[31]hhggh...hi<br>
289      * Data window can be cut before the end of the request but the process helps to obtain
290      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
291      * @param table which contains columns to find
292      * @return list of columns found
293      * @throws JSqlException when injection failure or stopped by user
294      */
295     public List<Column> listColumns(Table table) throws JSqlException {
296         List<Column> columns = new ArrayList<>();
297         
298         // Inform the view that table has just been used
299         var requestStartProgress = new Request();
300         requestStartProgress.setMessage(Interaction.START_INDETERMINATE_PROGRESS);
301         requestStartProgress.setParameters(table);
302         this.injectionModel.sendToViews(requestStartProgress);
303 
304         String resultToParse = StringUtils.EMPTY;
305         
306         try {
307             var pageSource = new String[]{ StringUtils.EMPTY };
308             resultToParse = new SuspendableGetRows(this.injectionModel).run(
309                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlColumns(table),
310                 pageSource,
311                 true,
312                 0,
313                 table,
314                 "columns"
315             );
316         } catch (AbstractSlidingException e) {
317             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
318         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
319             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
320         }
321 
322         // Build SQLite columns
323         if (this.injectionModel.getMediatorVendor().isSqlite()) {
324             resultToParse = this.injectionModel.getMediatorVendor().getSqlite().transformSqlite(resultToParse);
325         }
326         
327         // Parse all the data we have retrieved
328         var regexSearch = Pattern.compile(
329                 DataAccess.MODE
330                 + DataAccess.ENCLOSE_VALUE_RGX
331                 + DataAccess.CELL_TABLE
332                 + DataAccess.ENCLOSE_VALUE_RGX
333             )
334             .matcher(resultToParse);
335 
336         var requestEndProgress = new Request();
337         requestEndProgress.setMessage(Interaction.END_INDETERMINATE_PROGRESS);
338         requestEndProgress.setParameters(table);
339         this.injectionModel.sendToViews(requestEndProgress);
340 
341         if (!regexSearch.find()) {
342             throw new InjectionFailureException("No match while injecting columns");
343         }
344 
345         regexSearch.reset();
346 
347         // Build an array of Column objects from the data we have parsed
348         while (regexSearch.find()) {
349             String nameColumn = regexSearch.group(1);
350             var column = new Column(nameColumn, table);
351             columns.add(column);
352         }
353 
354         var requestAddColumns = new Request();
355         requestAddColumns.setMessage(Interaction.ADD_COLUMNS);
356         requestAddColumns.setParameters(columns);
357         this.injectionModel.sendToViews(requestAddColumns);
358         return columns;
359     }
360 
361     /**
362      * Get table values and count each occurrence and send it to the view.<br>
363      * Values are on clear text (not hexa) and follows this window pattern<br>
364      * => hh[value 1]jj[count]hhgghh[value 2]jj[count]hhggh...hi<br>
365      * Data window can be cut before the end of the request but the process helps to obtain
366      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
367      * @param columnsBean choice by the user
368      * @return a 2x2 table containing values by columns
369      * @throws JSqlException when injection failure or stopped by user
370      */
371     public String[][] listValues(List<Column> columnsBean) throws JSqlException {
372         var databaseBean = (Database) columnsBean.get(0).getParent().getParent();
373         var tableBean = (Table) columnsBean.get(0).getParent();
374         int rowCount = columnsBean.get(0).getParent().getChildCount();
375 
376         // Inform the view that table has just been used
377         var request = new Request();
378         request.setMessage(Interaction.START_PROGRESS);
379         request.setParameters(tableBean);
380         this.injectionModel.sendToViews(request);
381 
382         // Build an array of column names
383         List<String> columnsName = new ArrayList<>();
384         for (AbstractElementDatabase columnBean: columnsBean) {
385             columnsName.add(columnBean.toString());
386         }
387 
388         // From that array, build the SQL fields nicely
389         // => col1{%}col2...
390         // ==> trim(ifnull(`col1`,0x00)),0x7f,trim(ifnull(`Col2`,0x00))...
391         String[] columns = columnsName.toArray(new String[0]);
392 
393         List<List<String>> listValues = this.getRows(databaseBean, tableBean, rowCount, columns);
394 
395         // Add the default title to the columns: row number, occurrence
396         columnsName.add(0, StringUtils.EMPTY);
397         columnsName.add(0, StringUtils.EMPTY);
398 
399         String[][] table = this.getTable(columnsName, listValues);
400 
401         columns = columnsName.toArray(new String[0]);
402         
403         // Group the columns names, values and Table object in one array
404         var objectData = new Object[]{ columns, table, tableBean };
405 
406         var requestCreateValuesTab = new Request();
407         requestCreateValuesTab.setMessage(Interaction.CREATE_VALUES_TAB);
408         requestCreateValuesTab.setParameters(objectData);
409         this.injectionModel.sendToViews(requestCreateValuesTab);
410 
411         var requestEndProgress = new Request();
412         requestEndProgress.setMessage(Interaction.END_PROGRESS);
413         requestEndProgress.setParameters(tableBean);
414         this.injectionModel.sendToViews(requestEndProgress);
415         return table;
416     }
417 
418     private List<List<String>> getRows(Database database, Table table, int rowCount, String[] columns) throws InjectionFailureException {
419         String resultToParse = StringUtils.EMPTY;
420         
421         try {
422             var pageSource = new String[]{ StringUtils.EMPTY };
423             resultToParse = new SuspendableGetRows(this.injectionModel).run(
424                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlRows(columns, database, table),
425                 pageSource,
426                 true,
427                 rowCount,
428                 table,
429                 "rows"
430             );
431         } catch (AbstractSlidingException e) {  // Catch all exceptions but prevent detecting bug
432             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
433         } catch (Exception e) {
434             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
435         }
436         return SuspendableGetRows.parse(resultToParse);
437     }
438 
439     private static String getPartialResultAndLog(AbstractSlidingException e, String resultToParse) {
440         LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage());
441 
442         // Get pieces of data already retrieved instead of losing them
443         if (StringUtils.isNotEmpty(e.getSlidingWindowAllRows())) {
444             resultToParse = e.getSlidingWindowAllRows();
445         } else if (StringUtils.isNotEmpty(e.getSlidingWindowCurrentRows())) {
446             resultToParse = e.getSlidingWindowCurrentRows();
447         }
448         return resultToParse;
449     }
450 
451     private String[][] getTable(List<String> columnsName, List<List<String>> values) {
452         // Build a proper 2D array from the data
453         var table = new String[values.size()][columnsName.size()];
454         
455         for (var indexRow = 0 ; indexRow < values.size() ; indexRow++) {
456             var isIncomplete = false;
457             
458             for (var indexColumn = 0 ; indexColumn < columnsName.size() ; indexColumn++) {
459                 try {
460                     table[indexRow][indexColumn] = values.get(indexRow).get(indexColumn);
461                 } catch (IndexOutOfBoundsException e) {
462                     isIncomplete = true;
463                     LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, I18nUtil.valueByKey("LOG_LIST_VALUES_INCOMPLETE"));
464                     LOGGER.log(LogLevelUtil.IGNORE, e);
465                 }
466             }
467             
468             if (isIncomplete) {
469                 int logIndexRow = indexRow;
470                 LOGGER.log(
471                     LogLevelUtil.CONSOLE_ERROR,
472                     "{}{}: ",
473                     () -> I18nUtil.valueByKey("LOG_LIST_VALUES_TOO_LONG"),
474                     () -> logIndexRow + 1
475                 );
476 
477                 LOGGER.log(
478                     LogLevelUtil.CONSOLE_ERROR,
479                     () -> String.join(
480                         ", ",
481                         values.get(logIndexRow).toArray(new String[0])
482                     )
483                 );
484             }
485         }
486         return table;
487     }
488 }