1 /*
2  * Zed Attack Proxy (ZAP) and its related class files.
3  *
4  * ZAP is an HTTP/HTTPS proxy for assessing web application security.
5  *
6  * Copyright 2015 The ZAP Development Team
7  *
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 package org.zaproxy.zap.db.sql;
21 
22 import java.nio.charset.StandardCharsets;
23 import java.sql.Connection;
24 import java.sql.PreparedStatement;
25 import java.sql.ResultSet;
26 import java.sql.SQLException;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Vector;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import org.apache.logging.log4j.LogManager;
33 import org.apache.logging.log4j.Logger;
34 import org.parosproxy.paros.Constant;
35 import org.parosproxy.paros.db.Database;
36 import org.parosproxy.paros.db.DatabaseException;
37 import org.parosproxy.paros.db.DbUtils;
38 import org.parosproxy.paros.db.RecordHistory;
39 import org.parosproxy.paros.db.TableHistory;
40 import org.parosproxy.paros.extension.option.DatabaseParam;
41 import org.parosproxy.paros.model.HistoryReference;
42 import org.parosproxy.paros.network.HttpMalformedHeaderException;
43 import org.parosproxy.paros.network.HttpMessage;
44 import org.parosproxy.paros.network.HttpStatusCode;
45 
46 public class SqlTableHistory extends SqlAbstractTable implements TableHistory {
47 
48     private static final String TABLE_NAME = DbSQL.getSQL("history.table_name");
49 
50     private static final String HISTORYID = DbSQL.getSQL("history.field.historyid");
51     private static final String SESSIONID = DbSQL.getSQL("history.field.sessionid");
52     private static final String HISTTYPE = DbSQL.getSQL("history.field.histtype");
53     private static final String STATUSCODE = DbSQL.getSQL("history.field.statuscode");
54     private static final String TIMESENTMILLIS = DbSQL.getSQL("history.field.timesentmillis");
55     private static final String TIMEELAPSEDMILLIS = DbSQL.getSQL("history.field.timeelapsedmillis");
56     private static final String REQHEADER = DbSQL.getSQL("history.field.reqheader");
57     private static final String REQBODY = DbSQL.getSQL("history.field.reqbody");
58     private static final String RESHEADER = DbSQL.getSQL("history.field.resheader");
59     private static final String RESBODY = DbSQL.getSQL("history.field.resbody");
60     private static final String TAG = DbSQL.getSQL("history.field.tag");
61     private static final String NOTE = DbSQL.getSQL("history.field.note");
62     private static final String RESPONSE_FROM_TARGET_HOST =
63             DbSQL.getSQL("history.field.responsefromtargethost");
64 
65     private int lastInsertedIndex;
66     private static boolean isExistStatusCode = false;
67 
68     // ZAP: Added logger
69     private static final Logger log = LogManager.getLogger(SqlTableHistory.class);
70 
71     private boolean bodiesAsBytes;
72 
SqlTableHistory()73     public SqlTableHistory() {}
74 
75     // ZAP: Allow the request and response body sizes to be user-specifiable as far as possible
76     int configuredrequestbodysize = -1;
77     int configuredresponsebodysize = -1;
78 
79     @Override
reconnect(Connection conn)80     protected void reconnect(Connection conn) throws DatabaseException {
81         try {
82             // ZAP: Allow the request and response body sizes to be user-specifiable as far as
83             // possible
84             // re-load the configuration data from file, to get the configured length of the request
85             // and response bodies
86             // this will later be compared to the actual lengths of these fields in the database (in
87             // updateTable(Connection c))
88             DatabaseParam dbparams = new DatabaseParam();
89             dbparams.load(Constant.getInstance().FILE_CONFIG);
90             this.configuredrequestbodysize = dbparams.getRequestBodySize();
91             this.configuredresponsebodysize = dbparams.getResponseBodySize();
92 
93             bodiesAsBytes = true;
94 
95             if (DbSQL.getDbType().equals(Database.DB_TYPE_HSQLDB)) {
96                 updateTable(conn);
97             }
98 
99             isExistStatusCode = DbUtils.hasColumn(conn, TABLE_NAME, STATUSCODE);
100             int currentIndex = 0;
101             PreparedStatement stmt = null;
102             try {
103                 stmt = conn.prepareStatement(DbSQL.getSQL("history.ps.lastindex"));
104                 try (ResultSet rs = stmt.executeQuery()) {
105                     if (rs.next()) {
106                         currentIndex = rs.getInt(1);
107                     }
108                 }
109             } finally {
110                 if (stmt != null) {
111                     try {
112                         stmt.close();
113                     } catch (SQLException e) {
114                         if (log.isDebugEnabled()) {
115                             log.debug(e.getMessage(), e);
116                         }
117                     }
118                 }
119             }
120             lastInsertedIndex = currentIndex;
121         } catch (SQLException e) {
122             throw new DatabaseException(e);
123         }
124     }
125 
126     // ZAP: Added the method.
updateTable(Connection connection)127     private void updateTable(Connection connection) throws DatabaseException {
128         try {
129             if (!DbUtils.hasColumn(connection, TABLE_NAME, TAG)) {
130                 DbUtils.execute(connection, DbSQL.getSQL("history.ps.addtag"));
131             }
132 
133             // Add the NOTE column to the db if necessary
134             if (!DbUtils.hasColumn(connection, TABLE_NAME, NOTE)) {
135                 DbUtils.execute(connection, DbSQL.getSQL("history.ps.addnote"));
136             }
137 
138             /* TODO how to handle HSQLDB dependency?? Need to parameterize somehow.. vvvvvvvvvvvv */
139             if (DbUtils.getColumnType(connection, TABLE_NAME, REQBODY)
140                     != 61 /*Types.SQL_VARBINARY*/) {
141                 bodiesAsBytes = false;
142             } else {
143                 // Databases created with ZAP<1.4.0.1 used VARCHAR for the REQBODY/RESBODY
144                 // HSQLDB 1.8.x converted from VARCHAR to bytes without problems
145                 // (through the method ResultSet.getBytes)
146                 // but the new version doesn't, it throws the following exception:
147                 // incompatible data type in conversion: from SQL type VARCHAR
148             }
149 
150             if (!DbUtils.hasColumn(connection, TABLE_NAME, RESPONSE_FROM_TARGET_HOST)) {
151                 DbUtils.execute(connection, DbSQL.getSQL("history.ps.addrespfromtarget"));
152                 DbUtils.executeUpdate(connection, DbSQL.getSQL("history.ps.setrespfromtarget"));
153             }
154 
155             int requestbodysizeindb = DbUtils.getColumnSize(connection, TABLE_NAME, REQBODY);
156             int responsebodysizeindb = DbUtils.getColumnSize(connection, TABLE_NAME, RESBODY);
157             try {
158                 if (requestbodysizeindb != this.configuredrequestbodysize
159                         && this.configuredrequestbodysize > 0) {
160                     try (PreparedStatement stmt =
161                             connection.prepareStatement(DbSQL.getSQL("history.ps.changereqsize"))) {
162                         stmt.setInt(1, this.configuredrequestbodysize);
163                         stmt.execute();
164                     }
165                 }
166 
167                 if (responsebodysizeindb != this.configuredresponsebodysize
168                         && this.configuredresponsebodysize > 0) {
169                     try (PreparedStatement stmt =
170                             connection.prepareStatement(
171                                     DbSQL.getSQL("history.ps.changerespsize"))) {
172                         stmt.setInt(1, this.configuredresponsebodysize);
173                         stmt.execute();
174                     }
175                 }
176             } catch (SQLException e) {
177                 log.error("An error occurred while modifying a column length on " + TABLE_NAME);
178                 log.error(
179                         "The 'Maximum Request Body Size' value in the Database Options needs to be set to at least "
180                                 + requestbodysizeindb
181                                 + " to avoid this error");
182                 log.error(
183                         "The 'Maximum Response Body Size' value in the Database Options needs to be set to at least "
184                                 + responsebodysizeindb
185                                 + " to avoid this error");
186                 log.error("The SQL Exception was:", e);
187                 throw e;
188             }
189         } catch (SQLException e) {
190             throw new DatabaseException(e);
191         }
192     }
193 
194     @Override
read(int historyId)195     public RecordHistory read(int historyId)
196             throws HttpMalformedHeaderException, DatabaseException {
197         SqlPreparedStatementWrapper psRead = null;
198         try {
199             psRead = DbSQL.getSingleton().getPreparedStatement("history.ps.read");
200 
201             psRead.getPs().setInt(1, historyId);
202             psRead.getPs().execute();
203             RecordHistory result = null;
204             try (ResultSet rs = psRead.getPs().getResultSet()) {
205                 result = build(rs);
206             }
207 
208             return result;
209         } catch (SQLException e) {
210             throw new DatabaseException(e);
211         } finally {
212             DbSQL.getSingleton().releasePreparedStatement(psRead);
213         }
214     }
215 
216     @Override
write(long sessionId, int histType, HttpMessage msg)217     public RecordHistory write(long sessionId, int histType, HttpMessage msg)
218             throws HttpMalformedHeaderException, DatabaseException {
219 
220         String reqHeader = "";
221         byte[] reqBody = new byte[0];
222         String resHeader = "";
223         byte[] resBody = reqBody;
224         String method = "";
225         String uri = "";
226         int statusCode = 0;
227         String note = msg.getNote();
228 
229         if (!msg.getRequestHeader().isEmpty()) {
230             reqHeader = msg.getRequestHeader().toString();
231             reqBody = msg.getRequestBody().getBytes();
232             method = msg.getRequestHeader().getMethod();
233             uri = msg.getRequestHeader().getURI().toString();
234         }
235 
236         if (!msg.getResponseHeader().isEmpty()) {
237             resHeader = msg.getResponseHeader().toString();
238             resBody = msg.getResponseBody().getBytes();
239             statusCode = msg.getResponseHeader().getStatusCode();
240         }
241 
242         // return write(sessionId, histType, msg.getTimeSentMillis(), msg.getTimeElapsedMillis(),
243         // method, uri, statusCode, reqHeader, reqBody, resHeader, resBody, msg.getTag());
244         return write(
245                 sessionId,
246                 histType,
247                 msg.getTimeSentMillis(),
248                 msg.getTimeElapsedMillis(),
249                 method,
250                 uri,
251                 statusCode,
252                 reqHeader,
253                 reqBody,
254                 resHeader,
255                 resBody,
256                 null,
257                 note,
258                 msg.isResponseFromTargetHost());
259     }
260 
write( long sessionId, int histType, long timeSentMillis, int timeElapsedMillis, String method, String uri, int statusCode, String reqHeader, byte[] reqBody, String resHeader, byte[] resBody, String tag, String note, boolean responseFromTargetHost)261     private RecordHistory write(
262             long sessionId,
263             int histType,
264             long timeSentMillis,
265             int timeElapsedMillis,
266             String method,
267             String uri,
268             int statusCode,
269             String reqHeader,
270             byte[] reqBody,
271             String resHeader,
272             byte[] resBody,
273             String tag,
274             String note,
275             boolean responseFromTargetHost)
276             throws HttpMalformedHeaderException, DatabaseException {
277 
278         // ZAP: Allow the request and response body sizes to be user-specifiable as far as possible
279         if (reqBody.length > this.configuredrequestbodysize) {
280             throw new DatabaseException(
281                     "The actual Request Body length "
282                             + reqBody.length
283                             + " is greater than the configured request body length "
284                             + this.configuredrequestbodysize);
285         }
286         if (resBody.length > this.configuredresponsebodysize) {
287             throw new DatabaseException(
288                     "The actual Response Body length "
289                             + resBody.length
290                             + " is greater than the configured response body length "
291                             + this.configuredresponsebodysize);
292         }
293 
294         SqlPreparedStatementWrapper psInsert = null;
295         try {
296             psInsert = DbSQL.getSingleton().getPreparedStatement("history.ps.insertstd");
297             psInsert.getPs().setLong(1, sessionId);
298             psInsert.getPs().setInt(2, histType);
299             psInsert.getPs().setLong(3, timeSentMillis);
300             psInsert.getPs().setInt(4, timeElapsedMillis);
301             psInsert.getPs().setString(5, method);
302             psInsert.getPs().setString(6, uri);
303             psInsert.getPs().setString(7, reqHeader);
304             if (bodiesAsBytes) {
305                 psInsert.getPs().setBytes(8, reqBody);
306             } else {
307                 psInsert.getPs().setString(8, new String(reqBody, StandardCharsets.US_ASCII));
308             }
309             psInsert.getPs().setString(9, resHeader);
310             if (bodiesAsBytes) {
311                 psInsert.getPs().setBytes(10, resBody);
312             } else {
313                 psInsert.getPs().setString(10, new String(resBody, StandardCharsets.US_ASCII));
314             }
315             psInsert.getPs().setString(11, tag);
316 
317             // ZAP: Added the statement.
318             int currentIdx = 12;
319 
320             if (isExistStatusCode) {
321                 psInsert.getPs().setInt(currentIdx, statusCode);
322                 // ZAP: Added the statement.
323                 ++currentIdx;
324             }
325 
326             // ZAP: Added the statement.
327             psInsert.getPs().setString(currentIdx, note);
328             ++currentIdx;
329 
330             psInsert.getPs().setBoolean(currentIdx, responseFromTargetHost);
331 
332             psInsert.getPs().executeUpdate();
333 
334             try (ResultSet rs = psInsert.getLastInsertedId()) {
335                 rs.next();
336                 int id = rs.getInt(1);
337                 lastInsertedIndex = id;
338                 return read(id);
339             }
340         } catch (SQLException e) {
341             throw new DatabaseException(e);
342         } finally {
343             DbSQL.getSingleton().releasePreparedStatement(psInsert);
344         }
345     }
346 
build(ResultSet rs)347     private RecordHistory build(ResultSet rs)
348             throws HttpMalformedHeaderException, DatabaseException {
349         try {
350             RecordHistory history = null;
351             try {
352                 if (rs.next()) {
353                     byte[] reqBody;
354                     byte[] resBody;
355 
356                     if (bodiesAsBytes) {
357                         reqBody = rs.getBytes(REQBODY);
358                         resBody = rs.getBytes(RESBODY);
359                     } else {
360                         reqBody = rs.getString(REQBODY).getBytes();
361                         resBody = rs.getString(RESBODY).getBytes();
362                     }
363 
364                     history =
365                             new RecordHistory(
366                                     rs.getInt(HISTORYID),
367                                     rs.getInt(HISTTYPE),
368                                     rs.getLong(SESSIONID),
369                                     rs.getLong(TIMESENTMILLIS),
370                                     rs.getInt(TIMEELAPSEDMILLIS),
371                                     rs.getString(REQHEADER),
372                                     reqBody,
373                                     rs.getString(RESHEADER),
374                                     resBody,
375                                     rs.getString(TAG),
376                                     rs.getString(NOTE), // ZAP: Added note
377                                     rs.getBoolean(RESPONSE_FROM_TARGET_HOST));
378                 }
379             } finally {
380                 rs.close();
381             }
382             return history;
383         } catch (SQLException e) {
384             throw new DatabaseException(e);
385         }
386     }
387 
388     @Override
getHistoryIds(long sessionId)389     public List<Integer> getHistoryIds(long sessionId) throws DatabaseException {
390         return getHistoryIdsFromPreparedStatement(
391                 (wrapper) -> {
392                     wrapper.getPs().setLong(1, sessionId);
393                 },
394                 "history.ps.gethistoryids",
395                 null);
396     }
397 
getHistoryIdsFromPreparedStatement( PreparedStatementSetter preparedStatementSetter, String key, int... params)398     private List<Integer> getHistoryIdsFromPreparedStatement(
399             PreparedStatementSetter preparedStatementSetter, String key, int... params)
400             throws DatabaseException {
401         SqlPreparedStatementWrapper psGetHistoryIds = null;
402         try {
403 
404             psGetHistoryIds = DbSQL.getSingleton().getPreparedStatement(key, params);
405             preparedStatementSetter.setParameter(psGetHistoryIds);
406             List<Integer> v = new ArrayList<>();
407             try (ResultSet rs = psGetHistoryIds.getPs().executeQuery()) {
408                 while (rs.next()) {
409                     v.add(rs.getInt(HISTORYID));
410                 }
411             }
412             return v;
413         } catch (SQLException e) {
414             throw new DatabaseException(e);
415         } finally {
416             DbSQL.getSingleton().releasePreparedStatement(psGetHistoryIds);
417         }
418     }
419 
420     @Override
getHistoryIdsStartingAt(long sessionId, int startAtHistoryId)421     public List<Integer> getHistoryIdsStartingAt(long sessionId, int startAtHistoryId)
422             throws DatabaseException {
423         return getHistoryIdsFromPreparedStatement(
424                 (wrapper) -> {
425                     wrapper.getPs().setLong(1, sessionId);
426                     wrapper.getPs().setInt(2, startAtHistoryId);
427                 },
428                 "history.ps.gethistoryidsstartingat",
429                 null);
430     }
431 
432     @Override
433     public List<Integer> getHistoryIdsOfHistType(long sessionId, int... histTypes)
434             throws DatabaseException {
435         if (histTypes == null || histTypes.length == 0) {
436             return getHistoryIds(sessionId);
437         }
438 
439         return getHistoryIdsFromPreparedStatement(
440                 (wrapper) -> {
441                     wrapper.getPs().setLong(1, sessionId);
442                     DbSQL.setSetValues(wrapper.getPs(), 2, histTypes);
443                 },
444                 "history.ps.gethistoryidsinctypes",
445                 histTypes.length);
446     }
447 
448     @Override
449     public List<Integer> getHistoryIdsOfHistTypeStartingAt(
450             long sessionId, int startAtHistoryId, int... histTypes) throws DatabaseException {
451         if (histTypes == null || histTypes.length == 0) {
452             return getHistoryIds(sessionId);
453         }
454 
455         return getHistoryIdsFromPreparedStatement(
456                 (wrapper) -> {
457                     wrapper.getPs().setLong(1, sessionId);
458                     wrapper.getPs().setInt(2, startAtHistoryId);
459                     DbSQL.setSetValues(wrapper.getPs(), 3, histTypes);
460                 },
461                 "history.ps.gethistoryidsinctypesstartingat",
462                 histTypes.length);
463     }
464 
465     @Override
466     public List<Integer> getHistoryIdsExceptOfHistType(long sessionId, int... histTypes)
467             throws DatabaseException {
468         if (histTypes == null || histTypes.length == 0) {
469             return getHistoryIds(sessionId);
470         }
471 
472         return getHistoryIdsFromPreparedStatement(
473                 (wrapper) -> {
474                     wrapper.getPs().setLong(1, sessionId);
475                     DbSQL.setSetValues(wrapper.getPs(), 2, histTypes);
476                 },
477                 "history.ps.gethistoryidsnottypes",
478                 histTypes.length);
479     }
480 
481     @Override
482     public List<Integer> getHistoryIdsExceptOfHistTypeStartingAt(
483             long sessionId, int startAtHistoryId, int... histTypes) throws DatabaseException {
484         if (histTypes == null || histTypes.length == 0) {
485             return getHistoryIds(sessionId);
486         }
487 
488         return getHistoryIdsFromPreparedStatement(
489                 (wrapper) -> {
490                     wrapper.getPs().setLong(1, sessionId);
491                     wrapper.getPs().setInt(2, startAtHistoryId);
492                     DbSQL.setSetValues(wrapper.getPs(), 3, histTypes);
493                 },
494                 "history.ps.gethistoryidsnottypesstartingat",
495                 histTypes.length);
496     }
497 
498     @Override
499     public List<Integer> getHistoryList(
500             long sessionId, int histType, String filter, boolean isRequest)
501             throws DatabaseException {
502         try { // TODO
503             PreparedStatement psReadSearch =
504                     getConnection()
505                             .prepareStatement(
506                                     "SELECT * FROM HISTORY WHERE "
507                                             + SESSIONID
508                                             + " = ? AND "
509                                             + HISTTYPE
510                                             + " = ? ORDER BY "
511                                             + HISTORYID);
512             ResultSet rs = null;
513             Vector<Integer> v = new Vector<>();
514             try {
515 
516                 Pattern pattern =
517                         Pattern.compile(filter, Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
518                 Matcher matcher = null;
519 
520                 psReadSearch.setLong(1, sessionId);
521                 psReadSearch.setInt(2, histType);
522                 rs = psReadSearch.executeQuery();
523                 while (rs.next()) {
524                     if (isRequest) {
525                         matcher = pattern.matcher(rs.getString(REQHEADER));
526                         if (matcher.find()) {
527                             // ZAP: Changed to use the method Integer.valueOf.
528                             v.add(rs.getInt(HISTORYID));
529                             continue;
530                         }
531                         matcher = pattern.matcher(rs.getString(REQBODY));
532                         if (matcher.find()) {
533                             // ZAP: Changed to use the method Integer.valueOf.
534                             v.add(rs.getInt(HISTORYID));
535                             continue;
536                         }
537                     } else {
538                         matcher = pattern.matcher(rs.getString(RESHEADER));
539                         if (matcher.find()) {
540                             // ZAP: Changed to use the method Integer.valueOf.
541                             v.add(rs.getInt(HISTORYID));
542                             continue;
543                         }
544                         matcher = pattern.matcher(rs.getString(RESBODY));
545                         if (matcher.find()) {
546                             // ZAP: Changed to use the method Integer.valueOf.
547                             v.add(rs.getInt(HISTORYID));
548                             continue;
549                         }
550                     }
551                 }
552             } finally {
553                 if (rs != null) {
554                     try {
555                         rs.close();
556                     } catch (Exception e) {
557                         // Ignore
558                     }
559                 }
560                 psReadSearch.close();
561             }
562 
563             return v;
564         } catch (SQLException e) {
565             throw new DatabaseException(e);
566         }
567     }
568 
569     @Override
570     public void deleteHistorySession(long sessionId) throws DatabaseException {
571         SqlPreparedStatementWrapper psDeleteSession = null;
572         try {
573             psDeleteSession = DbSQL.getSingleton().getPreparedStatement("history.ps.deletesession");
574             psDeleteSession.getPs().setLong(1, sessionId);
575             psDeleteSession.getPs().executeUpdate();
576         } catch (SQLException e) {
577             throw new DatabaseException(e);
578         } finally {
579             DbSQL.getSingleton().releasePreparedStatement(psDeleteSession);
580         }
581     }
582 
583     @Override
584     public void deleteHistoryType(long sessionId, int historyType) throws DatabaseException {
585         SqlPreparedStatementWrapper psDeleteType = null;
586         try {
587             psDeleteType = DbSQL.getSingleton().getPreparedStatement("history.ps.deletetype");
588             psDeleteType.getPs().setLong(1, sessionId);
589             psDeleteType.getPs().setInt(2, historyType);
590             psDeleteType.getPs().executeUpdate();
591         } catch (SQLException e) {
592             throw new DatabaseException(e);
593         } finally {
594             DbSQL.getSingleton().releasePreparedStatement(psDeleteType);
595         }
596     }
597 
598     @Override
599     public void delete(int historyId) throws DatabaseException {
600         SqlPreparedStatementWrapper psDelete = null;
601         try {
602             psDelete = DbSQL.getSingleton().getPreparedStatement("history.ps.delete");
603             psDelete.getPs().setInt(1, historyId);
604             psDelete.getPs().executeUpdate();
605         } catch (SQLException e) {
606             throw new DatabaseException(e);
607         } finally {
608             DbSQL.getSingleton().releasePreparedStatement(psDelete);
609         }
610     }
611 
612     // ZAP: Added method.
613     @Override
614     public void delete(List<Integer> ids) throws DatabaseException {
615         delete(ids, 1000);
616     }
617 
618     @Override
619     public void delete(List<Integer> ids, int batchSize) throws DatabaseException {
620         if (ids == null) {
621             throw new IllegalArgumentException("Parameter ids must not be null.");
622         }
623         if (batchSize <= 0) {
624             throw new IllegalArgumentException("Parameter batchSize must be greater than zero.");
625         }
626 
627         SqlPreparedStatementWrapper psDelete = null;
628         try {
629             psDelete = DbSQL.getSingleton().getPreparedStatement("history.ps.delete");
630             int count = 0;
631             for (Integer id : ids) {
632                 psDelete.getPs().setInt(1, id);
633                 psDelete.getPs().addBatch();
634                 count++;
635 
636                 if (count % batchSize == 0) {
637                     psDelete.getPs().executeBatch();
638                     count = 0;
639                 }
640             }
641             if (count % batchSize != 0) {
642                 psDelete.getPs().executeBatch();
643             }
644         } catch (SQLException e) {
645             throw new DatabaseException(e);
646         } finally {
647             DbSQL.getSingleton().releasePreparedStatement(psDelete);
648         }
649     }
650 
651     /**
652      * @deprecated (2.5.0) Use {@link HistoryReference#addTemporaryType(int)} instead.
653      * @since 2.4
654      * @param historyType the history type that will be set as temporary
655      * @see #deleteTemporary()
656      */
657     @Deprecated
658     public static void setHistoryTypeAsTemporary(int historyType) {}
659 
660     /**
661      * @deprecated (2.5.0) Use {@link HistoryReference#removeTemporaryType(int)} instead.
662      * @since 2.4
663      * @param historyType the history type that will be marked as temporary
664      * @see #deleteTemporary()
665      */
666     @Deprecated
667     public static void unsetHistoryTypeAsTemporary(int historyType) {
668         HistoryReference.removeTemporaryType(historyType);
669     }
670 
671     @Override
672     public void deleteTemporary() throws DatabaseException {
673         SqlPreparedStatementWrapper psDeleteTemp = null;
674         try {
675             psDeleteTemp = DbSQL.getSingleton().getPreparedStatement("history.ps.deletetemp");
676             for (Integer type : HistoryReference.getTemporaryTypes()) {
677                 psDeleteTemp.getPs().setInt(1, type);
678                 psDeleteTemp.getPs().execute();
679             }
680         } catch (SQLException e) {
681             throw new DatabaseException(e);
682         } finally {
683             DbSQL.getSingleton().releasePreparedStatement(psDeleteTemp);
684         }
685     }
686 
687     @Override
688     public boolean containsURI(
689             long sessionId, int historyType, String method, String uri, byte[] body)
690             throws DatabaseException {
691         SqlPreparedStatementWrapper psContainsURI = null;
692         try {
693             psContainsURI = DbSQL.getSingleton().getPreparedStatement("history.ps.containsuri");
694             psContainsURI.getPs().setString(1, uri);
695             psContainsURI.getPs().setString(2, method);
696 
697             if (bodiesAsBytes) {
698                 psContainsURI.getPs().setBytes(3, body);
699             } else {
700                 psContainsURI.getPs().setString(3, new String(body));
701             }
702 
703             psContainsURI.getPs().setLong(4, sessionId);
704             psContainsURI.getPs().setInt(5, historyType);
705             try (ResultSet rs = psContainsURI.getPs().executeQuery()) {
706                 if (rs.next()) {
707                     return true;
708                 }
709             }
710             return false;
711         } catch (SQLException e) {
712             throw new DatabaseException(e);
713         } finally {
714             DbSQL.getSingleton().releasePreparedStatement(psContainsURI);
715         }
716     }
717 
718     @Override
719     public RecordHistory getHistoryCache(HistoryReference ref, HttpMessage reqMsg)
720             throws DatabaseException, HttpMalformedHeaderException {
721         try {
722             //  get the cache from provided reference.
723             //  naturally, the obtained cache should be AFTER AND NEARBY to the given reference.
724             //  - historyId up to historyId+200
725             //  - match sessionId
726             //  - history type can be MANUEL or hidden (hidden is used by images not explicitly
727             // stored in history)
728             //  - match URI
729             PreparedStatement psReadCache = null;
730 
731             // TODO
732             if (isExistStatusCode) {
733                 //          psReadCache = getConnection().prepareStatement("SELECT TOP 1 * FROM
734                 // HISTORY WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND " + HISTORYID + " >= ?
735                 // AND " + HISTORYID + " <= ? AND SESSIONID = ? AND (HISTTYPE = " +
736                 // HistoryReference.TYPE_MANUAL + " OR HISTTYPE = " + HistoryReference.TYPE_HIDDEN +
737                 // ") AND STATUSCODE != 304");
738                 psReadCache =
739                         getConnection()
740                                 .prepareStatement(
741                                         "SELECT TOP 1 * FROM HISTORY WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND "
742                                                 + HISTORYID
743                                                 + " >= ? AND "
744                                                 + HISTORYID
745                                                 + " <= ? AND SESSIONID = ? AND STATUSCODE != 304");
746 
747             } else {
748                 //          psReadCache = getConnection().prepareStatement("SELECT * FROM HISTORY
749                 // WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND " + HISTORYID + " >= ? AND " +
750                 // HISTORYID + " <= ? AND SESSIONID = ? AND (HISTTYPE = " +
751                 // HistoryReference.TYPE_MANUAL + " OR HISTTYPE = " + HistoryReference.TYPE_HIDDEN +
752                 // ")");
753                 psReadCache =
754                         getConnection()
755                                 .prepareStatement(
756                                         "SELECT * FROM HISTORY WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND "
757                                                 + HISTORYID
758                                                 + " >= ? AND "
759                                                 + HISTORYID
760                                                 + " <= ? AND SESSIONID = ?)");
761             }
762             psReadCache.setString(1, reqMsg.getRequestHeader().getURI().toString());
763             psReadCache.setString(2, reqMsg.getRequestHeader().getMethod());
764 
765             if (bodiesAsBytes) {
766                 psReadCache.setBytes(3, reqMsg.getRequestBody().getBytes());
767             } else {
768                 psReadCache.setString(3, new String(reqMsg.getRequestBody().getBytes()));
769             }
770 
771             psReadCache.setInt(4, ref.getHistoryId());
772             psReadCache.setInt(5, ref.getHistoryId() + 200);
773             psReadCache.setLong(6, ref.getSessionId());
774 
775             ResultSet rs = psReadCache.executeQuery();
776             RecordHistory rec = null;
777 
778             try {
779                 do {
780                     rec = build(rs);
781                     // for retrieval from cache, the message requests nature must be the same.
782                     // and the result should NOT be NOT_MODIFIED for rendering by browser
783                     if (rec != null
784                             && rec.getHttpMessage().equals(reqMsg)
785                             && rec.getHttpMessage().getResponseHeader().getStatusCode()
786                                     != HttpStatusCode.NOT_MODIFIED) {
787                         return rec;
788                     }
789 
790                 } while (rec != null);
791 
792             } finally {
793                 try {
794                     rs.close();
795                     psReadCache.close();
796                 } catch (Exception e) {
797                     // ZAP: Log exceptions
798                     log.warn(e.getMessage(), e);
799                 }
800             }
801 
802             // if cache not exist, probably due to NOT_MODIFIED,
803             // lookup from cache BEFORE the given reference
804 
805             // TODO
806             if (isExistStatusCode) {
807                 psReadCache =
808                         getConnection()
809                                 .prepareStatement(
810                                         "SELECT TOP 1 * FROM HISTORY WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND SESSIONID = ? AND STATUSCODE != 304");
811             } else {
812                 psReadCache =
813                         getConnection()
814                                 .prepareStatement(
815                                         "SELECT * FROM HISTORY WHERE URI = ? AND METHOD = ? AND REQBODY = ? AND SESSIONID = ?");
816             }
817             psReadCache.setString(1, reqMsg.getRequestHeader().getURI().toString());
818             psReadCache.setString(2, reqMsg.getRequestHeader().getMethod());
819 
820             if (bodiesAsBytes) {
821                 psReadCache.setBytes(3, reqMsg.getRequestBody().getBytes());
822             } else {
823                 psReadCache.setString(3, new String(reqMsg.getRequestBody().getBytes()));
824             }
825 
826             psReadCache.setLong(4, ref.getSessionId());
827 
828             rs = psReadCache.executeQuery();
829             rec = null;
830 
831             try {
832                 do {
833                     rec = build(rs);
834                     if (rec != null
835                             && rec.getHttpMessage().equals(reqMsg)
836                             && rec.getHttpMessage().getResponseHeader().getStatusCode()
837                                     != HttpStatusCode.NOT_MODIFIED) {
838                         return rec;
839                     }
840 
841                 } while (rec != null);
842 
843             } finally {
844                 try {
845                     rs.close();
846                     psReadCache.close();
847                 } catch (Exception e) {
848                     // ZAP: Log exceptions
849                     log.warn(e.getMessage(), e);
850                 }
851             }
852 
853             return null;
854         } catch (SQLException e) {
855             throw new DatabaseException(e);
856         }
857     }
858 
859     @Override
860     public void updateNote(int historyId, String note) throws DatabaseException {
861         SqlPreparedStatementWrapper psUpdateNote = null;
862         try {
863             psUpdateNote = DbSQL.getSingleton().getPreparedStatement("history.ps.updatenote");
864             psUpdateNote.getPs().setString(1, note);
865             psUpdateNote.getPs().setInt(2, historyId);
866             psUpdateNote.getPs().execute();
867         } catch (SQLException e) {
868             throw new DatabaseException(e);
869         } finally {
870             DbSQL.getSingleton().releasePreparedStatement(psUpdateNote);
871         }
872     }
873 
874     @Override
875     public int lastIndex() {
876         return lastInsertedIndex;
877     }
878 
879     private interface PreparedStatementSetter {
880         void setParameter(SqlPreparedStatementWrapper wrapper) throws SQLException;
881     }
882 }
883