I'm trying to use a macro to check and see if there are records left within a table, and if there are, then display a message, letting me know there is still data in the table. My set up looks like this
Currently, I'm getting this error message:
I get this error message regardless of whether I use Is Not Null or Not IsNull. Is there any suggestions as to what I'm doing wrong?
Macros is one of the, if not the, most annoying topics in Access. It illustrates beautifully one of the most galactically stupid aspects of Access: it cheerfully allows you to point to and select a table by name and a field name to boot giving you the false hope that it actually knows what you are talking about then turns around and tells you it has no idea what you are asking for. Brilliant. Not only does the "red headed step child" that is Access get mismatched shoes, neither of them fit!
I honestly thought this would be easy to answer. But to add insult to injury, Microsoft has documented this poorly conceived "feature" about as well as they have implemented it.
I'm not sure I understand exactly what you are trying to do but I think you simply want to know if a table is empty. If so, my suggestion: use a function. For example:
Function IsTableEmpty(aTableName As String) As Boolean
Dim rst As DAO.Recordset
Set rst = CurrentDb.OpenRecordset("select count(*) from [" & aTableName & "];", dbOpenForwardOnly, dbReadOnly)
IsTableEmpty = rst.Fields(0) = 0
rst.Close
Set rst = Nothing
End Function
If you insist on using a macro you will be astonished to find that Access can find your function and use it with no difficulty. You can use this with any table (query for that matter).
Sadly, if you are doing web applications with Access you are going to have to come to grips with whatever (lame) functionality macros provide because VBA will not be available to you.
HTH. Good luck.
PS - I would happily eat my words if someone would point me to the documentation that explains how this works and why such a situation exists!
Related
I am the maintainer (but thankfully not the creator) of a very old, very large and very badly written classic ASP site for an electronics manufacturer.
Security is a joke. This is the only thing done to sanitize input before throwing it into the mouth of MySQL:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = trim(txtval)
End Function
productid = txtval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Set rs = conn.execute(SQL)
Because of that, the site is unfortunately (but perhaps not surprisingly) victim of SQL injection attacks, some of them succesful.
The simple means taken above is not nearly enough. Nor is using Server.HTMLEncode. Escaping slashes doesn't help either as the attacks are quite sophisticated:
product.asp?id=999999.9+UnIoN+AlL+SeLeCt+0x393133353134353632312e39,0x393133353134353632322e39,0x393133353134353632332e39,0x393133353134353632342e39,0x393133353134353632352e39,0x393133353134353632362e39
The url above (an arbitrary attempt taken from the access log) gives the folling response from the site:
Microsoft OLE DB Provider for ODBC Drivers error '80004005'
[MySQL][ODBC 5.3(w) Driver][mysqld-5.1.42-community]
The used SELECT statements have a different number of columns
/product.asp, line 14
This means that the injection made it through but in this case did not succeed in getting any data. Others do, however.
The site consists of hundreds of ASP files with spaghetti code summing up to many thousands of lines without much structure. Because of that it is not an option to go for parameterized queries. The work would be enormous and error prone as well.
One good thing though is that all input parameters in the code are consistently passed through the txtval function, so here is a chance to do it better by augmenting the function. Also, since all SQL calls are done with conn.execute(SQL) it is quite straightforward to search and replace with eg. conn.execute(sanitize(SQL)) so here is a chance to do something about it too.
Given the circumstances, what are my options to prevent or at least minimize the risc of SQL injection?
Any input is much appreciated.
Updates:
1.
I do understand that parameterized queries is the correct way to handle the problem. I use that myself when I create websites. But given the way the site is built and the size of it, it will take 1-2 months to modify, test and debug it. Even if that is what we end up with (which I doubt) I need to do something right now.
2.
The replacement with the html entity is not a typo. It replaces single quote with its html entity. (I didn't make the code!)
3.
In the specific example above, using CInt(id) would solve the problem, but it could be anything, not only numerical inputs.
UPDATE 2:
Ok, I know that I am not asking for the correct solution. I knew that from the start. That's why I wrote "Given the circumstances".
But still, filtering inputs for mysql keywords like select, union etc would at least make it better. Not good, but a little bit better. And this is what I am asking for, ideas to make it a little bit better.
Although I appreciate your comments, telling me that the only good option is to use parameterized queries doesn't really help. Because I know that already :)
I wouldn't give up on parameterized queries. They are the single best tool you can use to protect yourself from SQL Injection. If your plan is to replace all of these calls:
conn.execute(SQL)
to these calls:
conn.execute(sanitize(SQL))
then you're already looking at modifying each interaction with SQL (BTW, don't forget Command.Execute() and Recordset.Open(), which may also be used to run SQL statements). And since you're already planning on changing these calls, consider calling a custom function to run the statement. For example, replace:
set rs = conn.execute(SQL)
with:
set rs = MyExecute(SQL)
and then use your custom function to set up a proper parameterized query using a Command object instead. You'll need to cleverly parse the SQL statement in this custom function. Identify the values in the where clause, determine their type (perhaps you can query the table schema), and add parameters accordingly. But it can be done.
You can also take this opportunity to sanitize the input. Use a RegExp object to quickly strip [^0-9\.] from numeric fields, for example.
But there's still the opportunity that you'll return a recordset from this function that will be used to write values directly to the page without being HTML-encoded first. That's a real concern, especially since it sounds like your site has already been targeted in the past. I wouldn't trust any data coming from your database. The only option I see here (that wouldn't involve touching every page) is to return a "clean", HTML-encoded recordset instead of the default one.
Unfortunately, you're still not out of the woods. XSS attacks can be done via QueryString parameters, cookies, and form controls. How safe are you going to feel after "fixing" the SQL Injection issues knowing that XSS is still a very real possibility?
My advice? Explain to your supervisor the security threats plaguing your site and convince him/her the need for a thorough review or a complete rewrite. It may seem like a lot of resources to throw at an "old, already-working website", but the moment someone defaces your website or truncates your database tables, you'll wish you invested the time.
This attack should only affect numeric values passed in your SQL.
There may or may not be a quick fix depending on whether the same txtval function is used for both numeric and string values (and others like date too).
If txtval is only used for numeric values (probably unlikely) then you could protected by adding single quotes around the value, eg:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = "'" & trim(txtval) & "'"
End Function
If it is used for all value types then your only option might be to search through all the code and either:
1) Add single quotes to all numeric SQL, eg:
SQL = "SELECT * FROM products WHERE id = '" & productid & "'"
2) Create a new function just for sanitizing number values and then change all your queries to use that (not a quick fix), eg:
Function numval(data)
If IsNumeric(data) Then
numvalue = CDbl(data)
Else
numvalue = 0 'or NULL?
End If
End Function
And then change your queries, eg:
productid = numval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Is there common code (ie. in an include file) that is used to open the database and create the conn variable used in your sample code?
If so, then you could just replace that code and create your own class with Open, Close and Execute functions (at least). You may need other methods too if they are used in your code.
That way you could effectively override the execute in lines like Set rs = conn.execute(SQL).
Eg:
Class MyDatabase
Private m_conn
Public Sub Open(connString)
Set m_conn = Server.CreateObject("ADODB.Connection")
m_conn.Open connString
End Sub
Public Sub Close()
m_conn.Close
Set m_conn = Nothing
End Sub
Public Function Execute(sql)
'Sanitize input here (sql), simple example just for this type of attack
If InStr(sql, "UnIoN AlL SeLeCt") <> 0 Then sql = ""
'return a RecordSet
Set Execute = m_conn.Execute(sql)
End Property
End Class
Then change your common conn declaration from... (eg)
Set conn = Server.CreateObject("ADODB.Connection")
...to...
Set conn = New MyDatabase
If you keep the txtval function I would also update it to escape slashes as well as single quotes, eg:
Function txtval(data)
txtval = Replace(Replace(strValue, "'", "''"), "\", "\\")
txtval = trim(txtval)
End Function
Hopefully something here might be of help.
I needed to copy some values from MS Access table into Excel using VBA code. I had done this many times and considered myself experienced. In my code I export the data using the following statements:
sh.range("A" & row).Value = rs("MyField")
where sh is Excel sheet, row is integer (long) and rs is recordset (either DAO or ADO, without any effect on the problem under consideration).
My code worked well on my computer with installed MS Office 2007. But when my client ran the code on his machine with MS Office 2010, it failed and kept failing very randomly. E.g. when debugging VBA in MS Access module step by step by pressing F8 it always worked. But when I pressed 'run' F5, it failed very soon.
After many trials and errors (I tried to open the recordset using different options and recordset types and caching the records etc.), I finally found that if I write
sh.range("A" & row).Value = rs("MyField").Value
everything works just fine. But according to the docs (e.g. http://msdn.microsoft.com/en-us/library/office/ff197799(v=office.15).aspx ) the Value property is the default property of the field object, which in turn is the default collection of the recordset object.
But it seems that I cannot rely on the defaultness, which I have been doing in most of my code. Actually I found a solution to my problem, but I still have no idea about the cause. Does anyone know why is the code doing this? What is the problem?
PS: I also found that if I expand the one-line statement into two lines (three with declaration):
dim v as Variant
v = rs("MyField")
sh.range("A" & row).Value = v
it also works...
Since rs("MyField") is a Field object, if you do ...
MsgBox TypeName(rs("MyField"))
... Access will tell you its type is Field.
So TypeName() is one example where the object itself is referenced directly instead of its default .Value property.
But something like Debug.Print always references .Value, so Debug.Print rs("MyField") is the same as Debug.Print rs("MyField").Value
If you know exactly when .Value will be referenced implicitly and when it will not, you can add it only when absolutely required and omit it the rest of the time.
However, some Access developers recommend always including .Value to avoid such confusion. If that seems like too much effort to you, at least consider including .Value when you do any assignment ...
something = rs("MyField").Value
... and be watchful for any other contexts where you don't get what you want without .Value
this is my first time posting a question here, but I almost always get good answers from searching this site. I'm trying to find out if there is a better way to speed up this process I have of writing a table column in Access to a .txt file. Here is the code I have, which works, but it's on the slow side. It takes about 45s to write around 7000 items.
lsFileName2 = "E:\DOI_Inventory2\SyncData\EquipUser.txt"
LiFileNumb = FreeFile
SysCmd acSysCmdSetStatus, "Updating User Equipment List"
Open lsFileName2 For Output As LiFileNumb
With rst
Do While Not .EOF
Write #LiFileNumb, ![EqUserFile]
.MoveNext
Loop
End With
dbs.Close
Close LiFileNumb
I'm fairly new to the IT field so any help would be greatly appreciated. Thanks
Just to add a note, the actual query is fine. I already checked that and it's pretty fast.
Create a query, save it with a name, and then use TransferText to export the query's data to your text file.
So assuming you have a SELECT query named qryExportMe like this which returns your table's column data correctly ...
SELECT EqUserFile
FROM YourTable;
... refer to this TransferText example and adapt it to fit your needs.
DoCmd.TransferText TransferType:=acExportDelim, _
Tablename:="qryExportMe", _
FileName:="E:\DOI_Inventory2\SyncData\EquipUser.txt", _
HasFieldNames:=True
Check the TransferText options at that linked page or from Access' built in help system.
Note you are not required to include the option names. I added them to help you keep track of which is which.
If this approach speeds up your export operation adequately, I think it will be because Access handles the task as a single set-based file write. Your recordset approach required Access to process one row at a time ... and one reason such approaches are called RBAR (row by agonizing row) is because they are often painfully slow.
Okay, friends, I'm leaving my job in a week and a half, and I'm trying to make what I've done easier for my boss to do. He has no access knowledge, so I'm trying to create a form that will automate the reports I've been generating. Rather than create a different form for all the different reports, I'm trying to automate it from a table of parameters. Here's what I'm going for:
I have a table, which I have created, which is comprised of 5 fields. I'd like to use these fields to fill parameter fields in a standard form template. The five fields in my table are as follows:
The type of query being run (the result spit out)
The queries that generate this report, separated by a comma and no space. "QRYNAMEA,QRYNAMEB"
The Table which these queries generate, which will be used by transferspreadsheet
The destination excel file, which already has a pivot table set up to feed of the data.
The input sheet of this excel file. Currently, all of these sheets are called "Input". (that isn't important)
My issue comes with having no idea where to go after I've made my combo box. I know enough visual basic to automate my queries, but not enough to populate the form with the information in 3,4 and 5 (so far, I've been manually changing these for different queries). I have no idea how to look up the record in the table from the choice in the 'choosebox', and then select individual fields from that in my automation.
I'm pretty confident in my ability to parse #2 and automate the queries, and to put the values into the fields I'm looking at, but I don't know how to actually pull those values from the table, before I can do these things. I also can't seem to describe this well enough for google to help me.
Has anyone done something like this before? I'm assuming I just lack knowledge of one of the VBA libraries, but I've not had any luck finding out which.
edit:
my inclination at this point is to create a query for this table, which will return a single field depending on the input I give. I can imagine doing this in SQL, but I still don't know how to populate the forms, nor extract the field object from the table once I get it.
I have to head out for the day, but I'll be back on Friday to keep working on this, and I'll post my solution, once I find it. This seems like a unique conundrum, and it would be nice to give an answer to it.
Final edit: code is polished (does not have much in the way of error handling):
The first method, which pulls the fields from the table and populates the form, is activated by choosing a new entry in the combo box and looks like this:
Private Sub QuerySelect_Change()
Dim db As Database
Dim rec As Recordset
Set db = CurrentDb
Set rec = db.OpenRecordset("SELECT [Queries to Run], [Source Table], [Destination Spreadsheet], [Destination Sheet Name] FROM TBL_QRY_SETTINGS WHERE TBL_QRY_SETTINGS.[Query Type] Like '" & [Forms]![QuerySelector]![QuerySelect] & "';")
[Forms]![QuerySelector]![QueriesToRun].Value = rec("Queries to Run")
[Forms]![QuerySelector]![SourceTable].Value = rec("Source Table")
[Forms]![QuerySelector]![FileDest].Value = rec("Destination Spreadsheet")
[Forms]![QuerySelector]![SheetName].Value = rec("Destination Sheet Name")
Set rec = Nothing
Set db = Nothing
End Sub
The second code pulls that data to run the query. I like how this turned out. It runs when a button near the combobox is clicked.
Private Sub DynamicQuery_Click()
Dim qryArray As Variant
Dim i As Integer
qryArray = Split([Forms]![QuerySelector]![QueriesToRun], ",")
DoCmd.SetWarnings False
For i = LBound(qryArray) To UBound(qryArray)
Debug.Print qryArray(i)
DoCmd.OpenQuery (qryArray(i))
Next
DoCmd.SetWarnings True
DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel12Xml, [Forms]![QuerySelector]![SourceTable], _
[Forms]![QuerySelector]![FileDest], _
True, [Forms]![QuerySelector]![SheetName]
End Sub
Note that the final code for part (1) is almost the same as the selected answer, except that I am grabbing more than one field. This works because I know that I have unique "Query Types", and my recordset will only contain one record.
Anyway, I hope some people stumble upon this and find it useful. Send me a message if you do. As far as I can tell from brief googling, this sort of automation work has not been done in access. It should make it easier for access-illiterate to run their own queries, and be simple for designers to add to, if they want all their queries available after a few clicks.
Someone could conceivably use this to automate a variety of reports in sequence, by iterating through a table like the one I reference.
I may be massively misunderstanding what you're doing, but I think it's as easy as creating a new form using the form wizard. It will let you choose the table that contains the data, and it will let you choose which fields you want to add.
You can later change any of the textboxes to combo boxes which will allow you to limit the choices available to fill in.
Am I understanding that correctly?
EDIT: This will fill a variable (MyRandomField) with the contents of a field in a table
Dim db as Database
Dim rec as Recordset
set db = CurrentDB
set rec = db.OpenRecordSet("Select SomeField from SomeTable Where Something = 'SomethingElse'")
MyRandomField = rec("SomeFieldName")
set rec = Nothing
set db = Nothing
I'm afraid I already know the answer to this, but I'm checking with the community in case there's something I don't know about....
Is it possible to have the MS Access table linking feature control rights to linked tables? Secifically, can FE.accdb have read/write privileges on BE_A.accdb, but read-only links to BE_B.accdb (without making BE_B.accdb entirely R/O)?
Sort of.
Quoting David Fenton from a post earlier this year on another forum:
One way would be to remove the linked
tables, and create queries to replace
them (you could use the same name for
the query as the corresponding linked
tables), and use a connect string in
the query and set the recordset type
to snapshot, which will be read-only
by default.
I just recently had occasion to do this myself. I asked a related question about setting the querydef recordset type to snapshot via VBA here.
You can adapt the following code to do what you need:
Sub ReadOnlyLink(MDBPath As String, TblName As String, SrcTblName As String)
Dim q As DAO.QueryDef
Set q = CurrentDb.CreateQueryDef(TblName, "SELECT * FROM " & SrcTblName & _
" IN """ & MDBPath & """")
q.Properties.Append q.CreateProperty("RecordsetType", dbByte, 2)
End Sub
This isn't really an answer, but it's too long for comments. Thoughts sparked by the answer I accepted.
It seems that it should be possible to use this for user-level "security":
Set up a BE
Set up a distributable FE with deliberately broken links
in the FE startup, get user name (API calls)
Run user name through a Select Case, re-linking as either real table or querydef as appropriate. Might be even better to do it with a user name -> role lookup before the Select.
Hmmm. Have to think about that some more.... Not bulletproof, but not really intended to be. More a way of dividing responsibility for updates to the users that are the respective domain experts. Don't know if it's really justified.