Creating SQL Where clause dynamically that is not open to SQL Injection - excel

I wrote this bit of VBA code that creates a SQL query dynamically based on the number of fields the user has selected and values read from an XL spreadsheet. It basically just adds "FIELD_VARIABLE=VALUE_VARIABLE OR" to the where clause and then removes the final OR after the loop ends.
It works for N number of fields added like I was hoping but my concern is security because I think I could just put like ';DROP TABLE Projects or some other malicious code into the spreadsheet from where the program is reading FIELD_VARIABLES. To a lesser extent since the query is different every time the execution path must be different and that probably slows down execution time.
I'm thinking of looking into parameterized queries or T-SQL to improve this. Was hoping one of you smart folks could point me in the right direction before I waste too much time on this. Here is the relevant VBA code:
'---loop through array of search fields and search values using the same index
'---since the arrays sizes will always be the same and create where filters dynamically
i = 1
For i = LBound(sLookupFields) To UBound(sLookupFields)
Set rngLookup = wsLookupSrc.cells(counter, lLookupCols(i))
'---clear where from last iteration through loop
SQLWhereDynamic = ""
SQLWhereDynamic = SQLWhereDynamic & " p." & sLookupFields(i) & " = '" + CStr(rngLookup.Value) & "' OR"
Next i
'---remove extra ' OR'
SQLWhereDynamic = Left(SQLWhereDynamic, (Len(SQLWhereDynamic) - 3))
SQLValue = wsLookupSrc.cells(counter, lLookupCols(1)).Value
SQLWhereDefault = "WHERE p.ClientId = " + CStr(iClientId) + ""
SQLQuery = SQLSelect + SQLWhereDefault + " AND (" + SQLWhereDynamic + ");"

Making the field name in the WHERE clause a parameter (and therefore dynamic and safe from injection) like you can with the value in the WHERE clause is impossible, I believe. However...
Here's how I would do it. Suppose you have an Excel range with all of the possible fields, search values filled in for those fields you want to search, and a data type (to be used in the code later). This below example shows two fields being searched
Field Value DataType
Sequence 131
CustomerID 200
InvoiceNumber 200
OrderNumber 200
InvoiceDate 8/14/2015 7
Item DS2 200
Location 200
ExportFile 200
DateImported 7
OnHold 11
The user fills in column 2. And the code builds the sql string
Sub MakeSQL()
Dim aSql(1 To 4) As String
Dim aWhere() As String
Dim vaFields As Variant
Dim lWhereCnt As Long
Dim lCnt As Long, i As Long
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Dim pm As ADODB.Parameter
'Skip number three until later
aSql(1) = "SELECT *"
aSql(2) = "FROM dbo.InvoiceLine"
aSql(4) = "ORDER BY InvoiceNumber DESC;"
'Grab all the search criteria
vaFields = Sheet1.Range("A2:C11").Value
'Set up the connection
Set cn = New ADODB.Connection
cn.Open sConn
Set cmd = New ADODB.Command
Set cmd.ActiveConnection = cn
'Count how many criteria where filled in
'You could Redim Preserve your aWhere() also
On Error Resume Next
lWhereCnt = Sheet1.Range("B2:B11").SpecialCells(xlCellTypeConstants).Count
On Error GoTo 0
'If there's at least one
If lWhereCnt >= 1 Then
ReDim aWhere(1 To lWhereCnt)
'Fill in an array and create parameters
For i = LBound(vaFields, 1) To UBound(vaFields, 1)
If Len(vaFields(i, 2)) > 0 Then
lCnt = lCnt + 1
'Put in the place holder
aWhere(lCnt) = vaFields(i, 1) & "=?"
'column 3 holds the data type
Set pm = cmd.CreateParameter(vaFields(i, 1) & "_p", vaFields(i, 3), adParamInput)
pm.Value = vaFields(i, 2)
'Variable length data types (I only use varchar, you may use more)
'must have a size specified
If vaFields(i, 3) = adVarChar Then pm.Size = Len(vaFields(i, 2))
cmd.Parameters.Append pm
End If
Next i
'Fill in the "where" section of your sql statement
aSql(3) = "WHERE " & Join(aWhere, " OR ")
End If
cmd.CommandText = Join(aSql, Space(1))
'Change this line to actually execute something
Debug.Print cmd.CommandText
For i = 0 To cmd.Parameters.Count - 1
Debug.Print , cmd.Parameters(i).Name, cmd.Parameters(i).Value
Next i
cn.Close
Set cn = Nothing
End Sub
For this example, the string comes out as
SELECT * FROM dbo.InvoiceLine WHERE InvoiceDate=? OR Item=? ORDER BY InvoiceNumber DESC;
InvoiceDate_p 8/14/2015
Item_p DS2

Related

Result from previous execution of INST_EXECUTE_REPORT via RFC "bleeds" into an a new execution if no results returned

I'm trying to automate certain procedures in SAP using Excel Templates and VBA.
To extract data, I'm using INST_EXECUTE_REPORT via RFC. I packed it into a separate Sub in VBA, in which I declare a local variable ObjR3_EXECUTE_REPORT:
Set ObjR3_EXECUTE_REPORT = ObjR3.Add("INST_EXECUTE_REPORT")
'Define SAP Tables
With ObjR3_EXECUTE_REPORT
Set ObjR3_EXECUTE_REPORT_Name = .Exports("PROGRAM")
Set ObjR3_EXECUTE_REPORT_Para = .Tables("PARA")
Set ObjR3_EXECUTE_REPORT_Result = .Tables("RESULT_TAB")
Set ObjR3_EXECUTE_REPORT_Output = .Tables("OUTPUT_TAB")
End With
'Define SAP source table
ObjR3_EXECUTE_REPORT_Name.Value = ReportName
FieldTablesArray = Array(ObjR3_EXECUTE_REPORT_Para, ObjR3_EXECUTE_REPORT_Result, ObjR3_EXECUTE_REPORT_Output) ',
For a = 0 To UBound(FieldTablesArray, 1)
Set InnerSAPTable = FieldTablesArray(a)
r = InnerSAPTable.RowCount
For f = 1 To r
InnerSAPTable.DeleteRow (1)
Next f
Next a
Dim aParameterPair() As Variant
Dim aParameterInput() As Variant
Dim sParameterName As String
Dim sParameterInput As String
'Build up the table with the fields to be sellected
f = 1
For a = LBound(aParameters) To UBound(aParameters)
aParameterPair = aParameters(a)
aParameterInput = aParameterPair(UBound(aParameterPair))
sParameterName = aParameterPair(LBound(aParameterPair))
For c = LBound(aParameterInput) To UBound(aParameterInput)
sParameterInput = aParameterInput(c)
ObjR3_EXECUTE_REPORT_Para.AppendRow
ObjR3_EXECUTE_REPORT_Para(f, "PARA_NAME") = sParameterName
ObjR3_EXECUTE_REPORT_Para(f, "PARA_VALUE") = sParameterInput
Debug.Print sParameterName & " " & sParameterInput
f = f + 1
Next c
Next a
CallResult = False
ObjR3_EXECUTE_REPORT.Imports("RESULT_TEXT").Clear
ObjR3_EXECUTE_REPORT_Output(1, 1) = sCaptionRow
'Call the ABAP function to extract the Report's output table to an object
CallResult = ObjR3_EXECUTE_REPORT.Call
'Debug.Print ReportName & ": " & ObjR3_EXECUTE_REPORT.Imports("RESULT_TEXT")
If CallResult = False Then
Debug.Print ObjR3_EXECUTE_REPORT.Imports("RESULT_TEXT")
Exit Sub
Else
Debug.Print ReportName & ": " & ObjR3_EXECUTE_REPORT_Output.RowCount & " data rows downloaded successfully)"
If ObjR3_EXECUTE_REPORT_Output.RowCount = 0 Then Exit Sub
End If
Please note that ObjR3 is a public variable:
Public ObjR3 As Object
Set ObjR3 = CreateObject("SAP.Functions")
To achieve the result I want, I first need to get the GL Account balances using program ReportName = "RFSSLD00" then, if the balance of a specific account is not 0, I want to download all individual transactions, but only in the last 60 days, using program ReportName = "RFITEMGL".
As long as there is at least one transaction in the last 60 days, the whole process works as expected. However, if there is no transaction in this time period, instead of returning a blank result, SAP returns the result of the ReportName = "RFSSLD00" program as soon as the ObjR3_EXECUTE_REPORT.Call function is executed.
I executed the routine in break mode, and before the second time ObjR3_EXECUTE_REPORT.Call is executed, the ObjR3_EXECUTE_REPORT_Output.RowCount of the Output table is 0. As soon as the call is ecexuted, so it was empty before the call.
If I reverse the order of both steps, i.e. first check for transactions in the last 60 days (RFITEMGL), then get the balance (RFSSLD00), the ObjR3_EXECUTE_REPORT_Output.RowCount for RFITEMGL is 0, as expected. However, I need the correct order for the procedure to make sense.
I'm at a loss here. Is there any way to make SAP "forget" the result of the first ObjR3_EXECUTE_REPORT.Call ? I tried removing the ObjR3_EXECUTE_REPORT, but it didn't work:
ObjR3.Remove ObjR3_EXECUTE_REPORT
Any suggestions would be welcome.
Best regards,

Fill shape data field from external data

I'm trying to link shape data field from external data like excel.
As #JohnGoldsmith suggested I used DropLinked but "I'm getting object name not found" error.
My main agenda is drop multiple shapes on drawing with shape data field "Name", then fill all the shape data field using external data in order. I also used spatial search for dropping shapes on drawing(Thanks to #Surrogate). By the way I'm using Visio Professional 2019.
It's often a good plan to separate chained members so you can identify whether (as #Paul points out) you're having a problem getting to the stencil or the master.
Following is a modified example of link shapes to data. I've ditched all of the spatial search stuff as I think that's a separate issue. If you still have trouble with that I would ask another question and narrow your sample code to not include the data linking part - ie just drop shapes and try and change their position. Bear in mind there's also Page.Layout and Selection.Layout
I think you've got the adding the DataRecordsets in the other linked question, so this example makes the following assumptions:
You have a drawing document open
You have the "Basic Shapes" stencil open (note my version is metric "_M")
You have a DataRecordset applied to the document named "AllNames"
The above record set has a column named "Name" that contains the data you want to link
Public Sub ModifiedDropLinked_Example()
Const RECORDSET_NAME = "AllNames"
Const COL_NAME = "Name"
Const STENCIL_NAME = "BASIC_M.vssx"
Const MASTER_NAME = "Rectangle"
Dim vDoc As Visio.Document
Set vDoc = Application.ActiveDocument
Dim vPag As Visio.Page
Set vPag = Application.ActivePage
Dim vShp As Visio.Shape
Dim vMst As Visio.Master
Dim x As Double
Dim y As Double
Dim xOffset As Double
Dim dataRowIDs() As Long
Dim row As Long
Dim col As Long
Dim rowData As Variant
Dim recordset As Visio.DataRecordset
Dim recordsetCount As Integer
For Each recordset In vDoc.DataRecordsets
If recordset.Name = RECORDSET_NAME Then
dataRowIDs = recordset.GetDataRowIDs("")
xOffset = 2
x = 0
y = 2
Dim vStencil As Visio.Document
Set vStencil = TryFindDocument(STENCIL_NAME)
If Not vStencil Is Nothing Then
Set vMst = TryFindMaster(vStencil, MASTER_NAME)
If Not vMst Is Nothing Then
For row = LBound(dataRowIDs) + 1 To UBound(dataRowIDs) + 1
rowData = recordset.GetRowData(row)
For col = LBound(rowData) To UBound(rowData)
Set vShp = vPag.DropLinked(vMst, x + (xOffset * row), y, recordset.ID, row, False)
Debug.Print "Linked shape ID " & vShp.ID & " to row " & row & " (" & rowData(col) & ")"
Next col
Next row
Else
Debug.Print "Unable to find master '" & MASTER_NAME & "'"
End If
Else
Debug.Print "Unable to find stencil '" & STENCIL_NAME & "'"
End If
Else
Debug.Print "Unable to find DataRecordset '" & RECORDSET_NAME & "'"
End If
Next
End Sub
Private Function TryFindDocument(docName As String) As Visio.Document
Dim vDoc As Visio.Document
For Each vDoc In Application.Documents
If StrComp(vDoc.Name, docName, vbTextCompare) = 0 Then
Set TryFindDocument = vDoc
Exit Function
End If
Next
Set TryFindDocument = Nothing
End Function
Private Function TryFindMaster(ByRef vDoc As Visio.Document, mstNameU As String) As Visio.Master
Dim vMst As Visio.Master
For Each vMst In vDoc.Masters
If StrComp(vMst.NameU, mstNameU, vbTextCompare) = 0 Then
Set TryFindMaster = vMst
Exit Function
End If
Next
Set TryFindMaster = Nothing
End Function
The above code drops six shapes onto the page and adds a Shape Data row (Prop._VisDM_Name) with the corresponding data value. If you want the name text to appear in the shape then you would normally modify the master with an inserted field in the shape's text. (If you get stuck with this part then ask another question.)
One last point is that this example loops through the DataRecordset rows dropping a shape for each one, but there is also a Page.DropManyLinkedU method that allows you to this en masse.

List values from sql search to Excel

I am running a search from Excel to get objects Internal ID's by External ID. In ThisWorkbook.Sheets("Other Data").Range("J30").Value I have External ID f5f9a21b-9208-de11-995f-005056bb3dfa. After search code should display Internal ID.
This one works and I am getting a message:
There were 3 objects with the display Id of f5f9a21b-9208-de11-995f-005056bb3dfa
How I can make this code to actually display these ID's for example starting from cell A1?
So instead of just a message:
MsgBox ("There were " & results.Count & " objects with the display Id of" & ThisWorkbook.Sheets("Other Data").Range("J30").Value)
I would get the ID's in Excel? Basically I need results.Count not to count items but input them to Excel.
Code edited according to suggestion, but debugger is pointing to ThisWorkbook.Sheets("Start").Cells(i, 1).Value = results(i)
Private Sub SurroundingSub()
Set oVault = oMFClientApp.BindToVault(szVaultName, 0, True, True)
' Create the condition.
Dim condition As New SearchCondition
Dim oScs: Set oScs = CreateObject("MFilesAPI.SearchConditions")
Dim oVaultConnections As MFilesAPI.VaultConnections
Dim i As Integer
' Set the expression.
condition.Expression.DataStatusValueType = MFStatusType.MFStatusTypeExtID
' Set the condition type.
condition.ConditionType = MFConditionType.MFConditionTypeEqual
' Set the value.
' In this case "MyExternalObjectId" is the ID of the object in the remote system.
condition.TypedValue.SetValue MFDataType.MFDatatypeText, ThisWorkbook.Sheets("Other Data").Range("J30").Value
'Add the condition to the collection.
oScs.Add -1, condition
'Search.
Dim results 'As ObjectSearchResults
Set results = oVault.ObjectSearchOperations.SearchForObjectsByConditions(oScs, MFSearchFlags.MFSearchFlagNone, False) ' False = SortResults
'Output the number of items matching (should be one in each object type, at a maximum).
'MsgBox ("There were " & results.Count & " objects with the display Id of" & ThisWorkbook.Sheets("Other Data").Range("J30").Value)
For i = 1 To results.Count
ThisWorkbook.Sheets("Start").Cells(i, 1).Value = results[i]
Next i
End Sub
EDIT 2
Also () does not work:
If you're looking for a quick and direct way try this:
For i = 1 To results.Count
Cells(i, 1).Value = results(i - 1)
Next i
Tip: Cells(row, column)

How do I retrieve a column of data as an array using vba - from an SSMS database

I am looking to return a a column of data to be used as a combo box within excel. To do this I am using a recordset to return my data from a stored procedure. The problem I am having is that I can't figure out how to populate or even display the information I am returning - or even if I am returning all the data.
I know there are solutions online that recommend using "rs.GetRows" to populate an array, however I have had no luck with this. I had originally intended to populate an array by indexing through the records in the recordset and adding them individually. The problem with this is I can never get the size of the recordset and because I am using dynamic SQL the size of my array is never the same. I have been at this for a while and I was wondering if there is a good way to do this.
VBA Code
Public Sub SQL_SP_Column_Data2(ByVal sql_col As String, _
ByVal sql_table As String)
On Error GoTo RunTimeError
Dim array1() As Variant
Dim sqlconxn As ADODB.connection
Dim sqlcmd As ADODB.Command ' Operates as a command between SQL and vba
Dim sqlrs As ADODB.recordSet
Dim conxnString As String
Application.ScreenUpdating = False
' String used to establish a connection to the database
conxnString = "driver={SQL Server};" & _
"server='server_name';" & _
"uid='username';" & _
"pwd='password';" & _
"database='database_name';"
Set sqlconxn = New ADODB.connection
Set sqlcmd = New ADODB.Command
Set sqlrs = New ADODB.recordSet
sqlconxn.ConnectionTimeout = 30
sqlconxn.Open conxnString ' makes the connection between SQL
MsgBox "Connection 1 state: " & GetState2(sqlconxn.state)
sqlcmd.CommandType = adCmdStoredProc ' sets command to a stored procedure
sqlcmd.CommandText = "GET_COLUMN_DATA" ' name of the stored procedure
sqlcmd.ActiveConnection = sqlconxn ' makes the sql connection
sqlcmd.Parameters.Append _
sqlcmd.CreateParameter("#column_name", adVarChar, adParamInput, 255, sql_col)
sqlcmd.Parameters.Append _
sqlcmd.CreateParameter("#data_table_name", adVarChar, adParamInput, 255, sql_table)
sqlrs.Open sqlcmd
If sqlrs.EOF Then
array1() = sqlrs.GetRows
Debug.Print "VALUES: " + array1(i)
sqlrs.MoveNext
i = i + 1
End If
RunTimeError: ' Reportd any errors that might occur in the system and
Dim strError As String
strError = "ERROR: " & Err.Number & vbCrLf & Err.Description
MsgBox strError
Debug.Print strError
Exit Sub
End Sub
I am using GetRows above because it was the most recommended method, but I can't get it to work the way I am intending it to. In this example I usually get ERROR: 0 or subscript out of range.
SQL Stored Procedure
USE AFCD
GO
ALTER PROCEDURE dbo.GET_COLUMN_DATA (#column_name nvarchar(max),
#data_table_name nvarchar(max)) AS
BEGIN
DECLARE #query nvarchar(max)
SET #query = 'SELECT ' + #column_name +
' FROM ' + #data_table_name + ''
EXEC sp_executesql #query
END
When I test this stored procedure I actually get the data that I want to I know my stored procedure is returning what I want, but I don't know how to get that within vba.
Example of expected outputs:
If the inputs into the the vba sub are - "WIRE_TYPE", "WIRE_INDEX"
SQL_SP_Column_Data2 "WIRE_TYPE", "WIRE_INDEX"
Then the output should look like this:
Stainless Steel
Steel ER70S-3
Steel ER70S-6
Titanium(1)
Titanium(2)
SOLUTION - This part of code refers to my vba and is right after I execute my SQL SP. Credit to Tim Williams for the answer.
If Not sqlrs.EOF Then
array1() = sqlrs.GetRows()
End If
Dim i As Integer
For i = 0 To 4
Debug.Print "VALUES: " + array1(0, i) ' 2D array!!!
Next i
Your If test is off, and GetRows doesn't need a loop:
If Not sqlrs.EOF Then
array1() = sqlrs.GetRows()
End If
Remember GetRows() returns a zero-based 2-D array, so you need to provide both dimensions when accessing values eg.
array1(0, 2) 'first field, third row

How can I export selected data to Excel from Access?

I am using the code from Function to export query or table to MS Excel to export all the data from one Access table to a worksheet in MS Excel.
This program stores time in and time out of employees in the table.
Let's say the admin wants to filter the data from 01 Jan 19 to 15 Jan 19.
I want to put two datepickers on my form as a basis for the "From" and "To".
I want to export that selected data. How can I inject that to this code?
Public Function Export2XL(InitRow As Long, DBAccess As String, DBTable As String) As Long
Dim cn As New ADODB.Connection 'Use for the connection string
Dim cmd As New ADODB.Command 'Use for the command for the DB
Dim rs2 As New ADODB.Recordset 'Recordset return from the DB
Dim MyIndex As Integer 'Used for Index
Dim MyRecordCount As Long 'Store the number of record on the table
Dim MyFieldCount As Integer 'Store the number of fields or column
Dim ApExcel As Object 'To open Excel
Dim MyCol As String
Dim Response As Integer
Set ApExcel = CreateObject("Excel.application") 'Creates an object
ApExcel.Visible = True 'This enable you to see the process in Excel
pExcel.Workbooks.Add 'Adds a new book.
ApExcel.ActiveSheet.Name = "" & (Export_data.Label1.Caption) & ""
'Set the connection string
cn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;data source=" &
app.Path & "\Dbase.mdb; User ID=admin;Persist Security Info=False;JET
OLEDB:Database Password=akgtrxx21"
'Open the connection
cn.Open
'Check that the connection is open
If cn.State = 0 Then cn.Open
Set cmd.ActiveConnection = cn
cmd.CommandText = DBTable
cmd.CommandType = adCmdTable
Set rs2 = cmd.Execute
'Count the number of fields or column
MyFieldCount = rs2.Fields.count
'Fill the first line with the name of the fields
For MyIndex = 0 To MyFieldCount - 1
ApExcel.Cells(InitRow, (MyIndex + 1)).Formula = rs2.Fields(MyIndex).Name
'Write Title to a Cell
ApExcel.Cells(InitRow, (MyIndex + 1)).Font.Bold = True
ApExcel.Cells(InitRow, (MyIndex + 1)).Interior.ColorIndex = 36
ApExcel.Cells(InitRow, (MyIndex + 1)).WrapText = True
Next
'Draw border on the title line
MyCol = Chr((64 + MyIndex)) & InitRow
ApExcel.Range("A" & InitRow & ":" & MyCol).Borders.Color = RGB(0, 0, 0)
MyRecordCount = 1 + InitRow
'Fill the excel book with the values from the database
Do While rs2.EOF = False
For MyIndex = 1 To MyFieldCount
ApExcel.Cells(MyRecordCount, MyIndex).Formula = rs2((MyIndex - 1)).Value
'Write Value to a Cell
ApExcel.Cells(MyRecordCount, MyIndex).WrapText = False 'Format the Cell
Next
MyRecordCount = MyRecordCount + 1
rs2.MoveNext
If MyRecordCount > 50 Then
Exit Do
End If
Loop
'Close the connection with the DB
rs2.Close
'Return the last position in the workbook
Export2XL = MyRecordCount
Set cn = Nothing
Set cmd = Nothing
Set rs2 = Nothing
Set ApExcel = Nothing
End Function
Excel does have a way to import data from Access with no VBA at all.
Create the connection to fill your worksheet.
Go to Menu Data > Access.
You will be asked to pick an Access database and select the table you want. You probably want a query to be executed but for now, pick any table; this will be edited later.
Edit the query to what you want.
Open the connection window by clicking on the menu Data > Connections and pick the connection you have just created. Then, go to the next tab (Definition), change Command Type from Table to SQL then in command text, type your command.
Don't close the window just yet.
Add condition on your date.
If the field is called, for instance, MyDate, then add a WHERE clause like this one: (MyDate >= ? AND MyDate <= ?).
When you refresh the data, you will be prompted to give values to replace the 2 question marks, and you will have the option to designate a cell to do it. You will also have an option for the query to always use what you have defined.
Note that when done correctly, you can reorder fields and/or create formulae in the table without causing any sort of problem to Excel at all. You can also create a Total row at the bottom to sum up values, using a formula (Excel will show you a dropdown to create a SUBTOTAL formula, that is conveniently sensitive to filters.
If you want to refresh data with VBA, it takes a single line of code to do: ThisWorkbook.Connections(...).Refresh or ApExcel.Workbooks(..).Connections(...).Refresh.
PS: If you absolutely want to keep your code above, then at least make sure not to copy rs2 cell by cell (that is way to slow due to Excel event handling) but rather, do something like: ApExcel.Cells(2, 1).CopyFromRecordset rs2

Resources