I'm starting to write my first Delphi application that connects to an SQL database (MySQL) using the ADO database components. I wondered whether there was any best way of storing the names of the fields in the database for easy reference when creating SQL queries later on.
First of all I made them a simple constant e.g. c_UserTable_Username, c_UserTable_Password, but then decided that was not a particularly good way of doing things so I am now storing them in a constant record e.g.:
type
TUserTable = record
TableName : String;
Username : String;
Password : String;
end;
const
UserTable : TUserTable =
(
TableName : 'users';
Username : 'Username';
Password : 'Password';
);
this allows me to create a statement like:
query.SQL.Add('SELECT ' + UserTable.Username + ' FROM ' + UserTable.TableName);
and not have to worry about hard coding the field names etc.
I've now run into the problem however where if I want to cycle through the table fields (for example if there are 20 or so fields), I can't. I have to manually type the record reference for every field.
I guess what I'd like to know is whether there is a way to iterate though all field names at once, or singularly; or am I going about this the wrong way? Perhaps I shouldn't be storing them like this at all?
Also, I've made a “Database” class which basically holds methods for many different SQL statements, for example GetAllUsers, GetAllProducts, etc. Does that sound correct? I've taken a look at a lot of Delphi/SQL tutorials, but they don't seem to go much past showing you how to run queries.
I suppose I'm just a little lost and any help is very welcome. Thanks :)
You could also store your queries as RESOURCESTRING which would allow editing of them after the fact using a resource editor (if necessary).
RESOURCESTRING
rsSelectFromUsers = 'SELECT USERNAME FROM USERS ';
Your approach of a database class works very well. I have done just that in several of my projects, returning an interface to an object which contains the dataset...the advantage of this is when the returned interface variable goes out of scope, the dataset would be closed and cleared.
Well, you are hard coding field names; you just hardcode them in the const instead of in the query itself. I'm not sure that actually improves anything. As far as iterating through the fields goes, try this:
var
Field: TField;
begin
for Field in query.Fields do begin
// do stuff with Field
end;
end;
Rather than making a "Database" class, I would probably use a TDataModule. This does almost the same thing as your class, except that it allows you to interactively design queries at design time. You can put any methods you need on the DataModule.
This also makes it really easy to instantiate persistent TFields (see help on that topic), which you may find the solution more to your liking than using consts to store field names.
If you're really going to use a database class as illustrated, consider the ability of records to contain functions in D2007 and later.
For instance, your example would become:
type
TUserTable = record
TableName : String;
Username : String;
Password : String;
function sqlGetUserName(where:string=''):string;
end;
const
UserTable : TUserTable =
(
TableName : 'users';
Username : 'Username';
Password : 'Password';
);
function TUserTable.sqlGetUserName(where:string=''): string;
begin
if where='' then result := Format('SELECT %s from %s', [userName, tableName])
else result := Format('SELECT %s from %s where %s', [userName, tableName, where]);
end;
which allows:
query.SQL.add(userTable.sqlGetUserName);
or
query.SQL.add(userTable.sqlGetUserName(Format('%s=%s', [userTable.userName,'BOB']));
I don't really recommend using SQL directly as you've illustrated. In my opinion, you should never have direct SQL calls to the tables. That's introducing a lot of coupling between the UI and the database (which shouldn't exist) and prevents you from placing a high level of security on direct table modification.
I would wrap everything into stored procs and have a DB interface class that encapsulates all of the database code into a data module. You can still use the direct links into data-aware components from a data module, you just have to preface the links with the DM name.
For instance, if you built a class like:
type
TDBInterface = class
private
function q(s:string):string; //just returns a SQL quoted string
public
procedure addUser(userName:string; password:string);
procedure getUser(userName:string);
procedure delUser(userName:string);
function testUser:boolean;
procedure testAllDataSets;
end;
function TDBInterface.q(s:string):string;
begin
result:=''''+s+'''';
end;
procedure TDBInterface.addUser(userName:string; password:string);
begin
cmd.CommandText:=Format( 'if (select count(userName) from users where userName=%s)=0 '+
'insert into users (userName, password) values (%s,%s) '+
'else '+
'update users set userName=%s, password=%s where userName=%s',
[q(userName), q(userName), q(password), q(userName), q(password), q(userName)]);
cmd.Execute;
end;
procedure TDBInterface.getUser(userName:string);
begin
qry.SQL.Add(Format('select * from users where userName=%s', [q(userName)]));
qry.Active:=true;
end;
procedure TDBInterface.delUser(userName:string);
begin
cmd.CommandText:=Format('delete from users where userName=%s',[userName]);
cmd.Execute;
end;
procedure TDBInterface.testAllDataSets;
begin
assert(testUser);
end;
function TDBInterface.testUser: boolean;
begin
result:=false;
addUser('99TEST99','just a test');
getUser('99TEST99');
if qry.IsEmpty then exit;
if qry.FieldByName('userName').value<>'99TEST99' then
exit;
delUser('99TEST99');
if qry.IsEmpty then
result:=true;
end;
You now have the ability to do some form of unit testing on your data interface, you've removed the SQL from the UI and things are looking up. You still have a lot of ugly SQL in your interface code though so move that over to stored procs and you get:
type
TDBInterface = class
public
procedure addUser(userName:string; password:string);
procedure getUser(userName:string);
procedure delUser(userName:string);
function testUser:boolean;
procedure testAllDataSets;
end;
procedure TDBInterface.addUser(userName:string; password:string);
begin
cmd.CommandText:='usp_addUser;1';
cmd.Parameters.Refresh;
cmd.Parameters.ParamByName('#userName').Value:=userName;
cmd.Parameters.ParamByName('#password').Value:=password;
cmd.Execute;
cmd.Execute;
end;
procedure TDBInterface.getUser(userName:string);
begin
sproc.Parameters.ParamByName('#userName').Value:=userName;
sproc.Active:=true;
end;
procedure TDBInterface.delUser(userName:string);
begin
cmd.CommandText:='usp_delUser;1';
cmd.Parameters.Refresh;
cmd.Parameters.ParamByName('#userName').Value:=userName;
cmd.Execute;
end;
You could now move some of these functions into an ADO Thread and the UI would have no idea that adding or deleting users occur in a separate process. Mind, these are very simple operations so if you want to do handy things like notifying the parent when process are done (to refresh a user list for instance after add/delete/update occurs), you'd need to code that into the threading model.
BTW, the stored proc for the add code looks like:
create procedure [dbo].[usp_addUser](#userName varchar(20), #password varchar(20)) as
if (select count(userName) from users where userName=#userName)=0
insert into users (userName, password) values (#userName,#password)
else
update users set userName=#userName, password=#password where userName=#userName
Also, a little disclaimer: this post is pretty long and, while I tried to check most of it, I may have missed something, somewhere.
Maybe a bit off topic but you could use Data Abstract from RemObjects.
Take a loot at Analysing DataSets (in Delphi and Kylix)
The code is a good example of manipulating table metadata. You can get field names and then write a code-generator that can create a base unit/ or the interface part of it.
Related
After learning about dynamic queries, I'm trying to build a couple of base functions C(reate)R(ead)U(pdate)d(elete)
for my db saving me to repeat a lot of code for table/view.
I want to take advantage of roles and apply security at database layer so to have a dumb UI that only connects and interact with available functions for that roles the user belongs.
I would really appreciate any advice, critic, correction that community of advanced developers could contribute.
This is a continuation of my learning path in Postgres 13.
How secure is format() for dynamic queries inside a function?
A brief of my approach to accomplish this is:
ROLES
role owner
All database, schemas, tables, functions, seq, etc... belongs to it.
Everyone is revoked everything over these listed objects.
No user/role belong to this role.
role login
Only granted connection to database.
role manager
Only granted a subset of wrapper functions.
role seller
Only granted another subset of wrapper functions.
USERS
User1 - Has login and manager.
User2 - Has login and seller.
BASE FUNCTION
This is a base function for insert.
Owned by owner.
Everyone revoked anything on it.
Thought to only to be ran by wrapper function owned by owner.
Parameter 1 is the table where to work and always will be hardcoded at wrapper.
Parameter 2 is a list of allowed columns for operation and always hardcoded at wrapper.
Parameter 3 is a json payload with key and values, some of them could be valuable for function itself, others validated against parameter 2 used in the query building and the rest just ignored.
CREATE FUNCTION public.crud__insert (IN _tbl text, IN _cols text[], IN _opts json, OUT _id int)
LANGUAGE plpgsql STRICT AS
$$
DECLARE
BEGIN
EXECUTE (
SELECT concat('INSERT INTO '
, _tbl
, '('
, string_agg(e.key, ', ' ORDER BY ord)
, ') VALUES ('
, string_agg(format('%L', e.val), ', ' ORDER BY ord)
, ') RETURNING id'
)
FROM json_each_text(_opts) WITH ORDINALITY e(key, val, ord)
WHERE e.key = ANY(_cols)
) INTO _id;
END;
$$;
WRAPPER FUNCTION
This is a wrapper function for one specific table or view.
Owned by owner.
Everyone revoked anything on it.
manager granted select.
Parameter 1 is the json payload to use as parameter 3 in base function.
CREATE FUNCTION public.entity__insert (IN _opts json, OUT _id int)
LANGUAGE sql STRICT SECURITY DEFINER AS
$$
SELECT public.crud__insert(
'public.entity',
ARRAY['name', 'provider', 'customer'],
_opts
);
$$;
You run it by:
SELECT public.entity__insert('{"name": "Richard", "provider": true}');
And it gives you new id.
entity__insert
158
I am using Delphi 7, Windows 7 and Absolute Database.
Quick Background. I work for a charity shop that relies on items being donated to us. To reclaim 'Gift Aid' from our HMRC we have to submit details of all sales together with the name and address of each donator of that sale. We help people with Special Needs so accurate data input is important.
Up to now to check Post Code verification was easy (based on our local area) basically the format of AA00_0AA or AA0_0AA. As our name has become better known not all Post Codes follow these rules.
I have access to UK's Royal Mail database for addresses in the UK si I thought to actually compare the inputted Post Code with a real Post Code. The RM csv file is huge so I use GSplit3 to break it down into more manageable files. This leaves me with 492 csv files each consisting of about 62000 lines. Note that I am only interested in the Post Codes so there is massive duplication.
To load these files into a dataset (without duplication) I first loaded the file names into a listbox and ran a loop to iterate through all the files to copy to the dataser.To avoid duplication I tried putting an unique index on the field but even running outside of Delphi I still got an error message about duplication. I then tried capturing the text of the last record to be appended and then compare it with the next record
procedure TForm1.importClick(Sender: TObject);
var
i,y:Integer;
lstfile:string;
begin
for i:= 0 to ListBox1.Items.Count-1 do
begin
lstfile:='';
cd.Active:=False;//cd is a csv dataset loaded with csv file
cd.FileName:='C:\paf 112018\CSV PAF\'+ListBox1.Items[i];
cd.Active:=True;
while not cd.Eof do
begin
if (lstfile:=cd.Fields[0].AsString=cd.Fields[0].AsString) then cd.Next
else
table1.append;
table1.fields[0].asstring:=cd.Fields[0].AsString;
lstfile:=cd.Fields[0].AsString;
cd.Next;
end;
end;
table1.Edit;
table1.Post;
end;
This seemed to work OK although the total number of records in the dataset seemed low. I checked with my own Post Code and it wasn't there although another post Code was located. So obviously records had been skipped. I then tried loading the CSV file into a string list with dupignore then copying the stringlist to the dataset.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, DBGrids, SMDBGrid, StdCtrls, DB, ABSMain, SdfData;
type
TForm1 = class(TForm)
cd: TSdfDataSet;
dscd: TDataSource;
dst: TDataSource;
ABSDatabase1: TABSDatabase;
table1: TABSTable;
table1PostCode: TStringField;
Label2: TLabel;
ListBox1: TListBox;
getfiles: TButton;
import: TButton;
procedure getfilesClick(Sender: TObject);
procedure importClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
num:Integer;
implementation
{$R *.dfm}
procedure ListFileDir(Path: string; FileList: TStrings);
var
SR: TSearchRec;
begin
if FindFirst(Path + '*.csv', faAnyFile, SR) = 0 then
begin
repeat
if (SR.Attr <> faDirectory) then
begin
FileList.Add(SR.Name);
end;
until FindNext(SR) <> 0;
FindClose(SR);
end;
end;
procedure TForm1.getfilesClick(Sender: TObject);
begin//Fill listbox with csv files
ListFileDir('C:\paf 112018\CSV PAF\', ListBox1.Items);
end;
//start to iterate through files to appane to dataset
procedure TForm1.importClick(Sender: TObject);
var
i,y:Integer;
myl:TStringList;
begin
for i:= 0 to ListBox1.Items.Count-1 do
begin
myl:=TStringList.Create;
myl.Sorted:=True;
myl.Duplicates:=dupIgnore;
cd.Active:=False;
cd.FileName:='C:\paf 112018\CSV PAF\'+ListBox1.Items[i];
cd.Active:=True;
while not cd.Eof do
begin
if (cd.Fields[0].AsString='')then cd.Next
else
myl.Add(cd.Fields[0].AsString);
cd.Next;
end;
for y:= 0 to myl.Count-1 do
begin
table1.Append;
table1.Fields[0].AsString:=myl.Strings[y];
end;
myl.Destroy;
end;
t.Edit;
t.Post;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
t.Locate('Post Code',edit1.text,[]);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
sel:string;
begin
q.Close;
q.SQL.Clear;
q.SQL.Add('Select * from postc where [Post Code] like :sel');
q.ParamByName('sel').AsString:=Edit1.Text;
q.Active:=True;
end;
end.
This starts well but soon starts to slow I presume because of memory leaks, I have tried myl.free(), freeandnil(myl) and finally destroy but they all slow down quickly. I am not an expert but do enjoy using Delphi and normally manage to solve problems through your pages or googling but this time I am stumped. Can anyone suggest a better method please
The following shows how to add postcodes from a file containing a list of them
to a query-type DataSet such as TAdoQuery or your q dataset (your q doesn't say what type
it is, as far as I can see).
It also seems from what you say that although you treat your postcode file
as a CVS file, you shouldn't actually need to: if the records contain only one
field, there should be no need for commas, because there is nothing to separate,
the file should contain simply one postcode per line. Consequently, there
doesn't seem to be any need to incur the overhead of loading it as a CSV file, so you should be able simply to load it into a TStringList and add the postcodes from there.
I'm not going to attempt to correct your code, just show a very simple example of how I think it should be done.
So the following code opens a postcode list file, which is assumed to contain one postcode per line, checks whether each entry in it
already exists in your table of postcodes and adds it if not.
procedure TForm1.AddPostCodes(const PostCodeFileName: String);
// The following shows how to add postcodes to a table of existing ones
// from a file named PostCodeFileName which should include an explicit path
var
PostCodeList : TStringList;
PostCode : String;
i : Integer;
begin
PostCodeList := TStringList.Create;
try
PostCodeList.LoadFromFile(PostCodeFileName);
if qPostCodes.Active then
qPostCodes.Close;
qPostCodes.Sql.Text := 'select * from postcodes order by postcode';
qPostCodes.Open;
try
qPostCodes.DisableControls; // Always call DisableControls + EnableControls
// when iterating a dataset which has db-aware controls connected to it
for i := 0 to PostCodeList.Count - 1 do begin
PostCode := PostCodeList[i]; // use of PostCode local variable is to assist debuggging
if not qPostCodes.Locate('PostCode', PostCode, [loCaseInsensitive]) then
qPostCodes.InsertRecord([PostCode]); // InsertRecord does not need to be foollowed by a call to Post.
end;
finally
qPostCodes.EnableControls;
qPostCodes.Close;
end;
finally
PostCodeList.Free; // Don't use .Destroy!
end;
end;
Btw, regarding the comment about bracketing an iteration of a dataset
by calls to DisableControls and EnableControls, the usual reason for doing this is to avoid the overheaad of updating the gui's display of any db-aware controls connected to the dataset. However, one of the reasons I'm
not willing to speculate what is causing your slowdown is that TAdoQuery,
which is one of the standard dataset types that codes with Delphi benefits massively
from DisableControls/EnableControls even when there are NO db-aware controls
connected to it. This is because of a quirk in the coding of TAdoQuery. Whatever
dataset you are using may have a similar quirk.
I notice that many people have said it's possible to create an injection attack, but my understanding is that is if someone is creating a query from a string, not parameters. In order to test the statement that Stored Procedures do not protect you against Injection Attacks, I am putting this example up in the hopes someone can show me a vulnerability if there is one.
Please note that I have built the code this way to easily insert a function that calls a procedure and embed it in a SELECT query. That means I cannot create a Prepared Statement. Ideally I'd like to keep my setup this way, as it is dynamic and quick, but if someone can create an injection attack that works, obviously that is not going to happen.
DELIMITER $$
#This procedure searches for an object by a unique name in the table.
#If it is not found, it inserts. Either way, the ID of the object
#is returned.
CREATE PROCEDURE `id_insert_or_find` (in _value char(200), out _id bigint(20))
BEGIN
SET #_value = _value;
SET #id = NULL;
SELECT id INTO _id FROM `table` WHERE name=_value;
IF _id IS NULL THEN
BEGIN
INSERT INTO `table` (`name`) VALUE (_value);
SELECT LAST_INSERT_ID() INTO _id;
END;
END IF;
END$$
CREATE FUNCTION `get_id` (_object_name char(200)) RETURNS INT DETERMINISTIC
BEGIN
SET #id = NULL;
call `id_insert_or_find`(_object_name,#id);
return #id;
END$$
The PHP Code
The PHP code I use here is:
(note, Boann has pointed out the folly of this code, below. I am not editing it for the sake of honoring the answer, but it will certainly not be a straight query in the code. It will be updated using ->prepare, etc. I still welcome any additional comments if new vulnerabilities are spotted.)
function add_relationship($table_name,$table_name_child) {
#This table updates a separate table which has
#parent/child relationships listed.
$db->query("INSERT INTO table_relationships (`table_id`,`tableChild_id`) VALUES (get_id('{$table_name}'),get_id('{$table_name_child}')");
}
The end result is
table `table`
id name
1 oak
2 mahogany
Now if I wanted to make oak the child of mahogany, I could use
add_relationship("mahogany","oak");
And if I wanted to make plastic the child of oak, I could use
add_relationship("oak","plastic");
Hopefully that helps give some framework and context.
It is not necessarily the stored procedure that is unsafe but the way you call it.
For example if you do the following:
mysqli_multi_query("CALL id_insert_or_find(" + $value + ", " + $id + ")");
then the attacker would set $value="'attack'" and id="1); DROP SCHEMA YOUR_DB; --"
then the result would be
mysqli_multi_query("CALL id_insert_or_find('attack', 1); DROP SCHEMA YOUR_DB; --)");
BOOM DEAD
Strictly speaking, that query should be written to escape the table names:
$db->query("INSERT INTO table_relationships (`table_id`,`tableChild_id`) " .
"VALUES (get_id(" . $db->quote($table_name) + ")," .
"get_id(" . $db->quote($table_name_child) . "))");
Otherwise, it would break out of the quotes if one of the parameters contained a single quote. If you only ever call that function using literal strings in code (e.g., add_relationship("mahogany", "oak");) then it is safe to not escape it. If you might ever call add_relationship using data from $_GET/$_POST/$_COOKIE or other database fields or files, etc, it's asking for trouble. I would certainly not let it pass a code review.
If a user could control the table name provided to that function then they could do, for example:
add_relationship("oak", "'+(SELECT CONCAT_WS(',', password_hash, password_salt) FROM users WHERE username='admin')+'");
Now you might say that there's no practical way to then extract that information if the resulting table name doesn't exist, but even then you could still extract information one binary bit at a time using a binary search and separate queries, just by breaking the query. Something like this (exact syntax not tested):
add_relationship("oak", "plastic'+(IF(ORD(SUBSTR(SELECT password_hash FROM users WHERE username='admin'),1,1)>=128, 'foo', ''))+'");
Really, it's easier to just escape the parameters and then you don't have to worry.
I am using a database someone else produced (and I am not really authorised to change it). However, as I was looking into the stored procedures within the database I noticed the following procedure:
DELIMITER $$
CREATE PROCEDURE `logIn`(userName varChar(50), userPass varChar(50))
BEGIN
declare userID int;
SELECT
u.userID INTO userID
FROM
users u
WHERE
u.userName=userName
AND u.userPassword=MD5(userPass);
IF (IFNULL(uID,-1) > 0) THEN
select 1 as outMsg;
ELSE
select 0 as outMsg;
END IF;
END$$
with the corresponding table users having three columns: userID INT, userName VARCHAR(50) and userPassword VARCHAR(50).
As I am not very good at this, could someone let me know whether the input for such a function needs to be sanitised as to not allow any SQL injections and if not - why? A general rule of thumb would be very much appreciated.
P.S. This function will be called from a JS script on a form submit.
There are a few rules of thumb here that depend on the underlying datatype and how it's inserted into the database.
First, Parameterized queries are always best for SQL Injection protection.. but.. if you can't change that..
String type:
Remove any single quotes
OR
Replace any single quotes with the single quote twice.
Replace any of the following characters with their encoded alternative;
>
<
"
;
(chr 34)
)
(
For example.. ) is replaced with & #x29;
-(the space in the above example is so you'll see the code, remove it to get ")")
For a datatype other then string, check that the datatype is sane and remove any character that shouldn't be in the datatype. If it's an integer, make sure the string that you're passing in is an integer. This can commonly be done by casting to the type in code. The cast will either work.. or cause an error. It's also good to check that the datatype min and maxes have not been exceeded. For example.. If I was checking for an integer, I might use code similar to this:
var myInt = parseInt(param);
Then I might check it's bounds to be sure it's less then the maximum integer value and greater then the minimum integer value.
That should be good enough to prevent a SQL Injection attack...
And.. since you have not posted the code that actually interfaces with the database... As an added precaution.. you may also want to remove --,`,%,",", "".
You only want 'sane' values getting to the database call.. so an integer like, $309 wouldn't make sense, you'd want to remove the $.. . probably by using a regex replace for any non numeric characters a comma and a period.
[^[0-9,.]]
Be extra cautious.
Yes, the input must be sanitized before trying to run the procedure.
You might want to share the actual calling point for the procedure to get more help here, since there is no way that the procedure is called directly from JS on form submit. You probably have a Servlet, PHP page or some HTTP friendly intermediary to make the database call somehow.
This is something new for me to try using XML column in SQL server 2008 database. I have seen few posts related to this, but I am able to find it difficult.
Let me put my question in a simplest form.
I have a database table dbo.cXML that has the columns EmailID(nVarchar(128)), ClientID(int) and cycleXML(XML).
My middleware component implements complex business logic and spit out the XML after logic processing.
Now I have a requirement that need the following:
a) A stored procedure with parameters in place to perform a check on above table to see if there is already an XML for a given EmailID and Client ID. If a record exists, use Update query to update entire XML otherwise simply insert the XML.
b) A stored procedure whould be able to send back the complete XML to my middleware component on request.
Can someone please help me understand the pseudo code. Appreciate your help.
Thanks,
Yagya
Not totally sure what you mean by requirement 3b but does this help you???
IF EXISTS (SELECT 1 FROM dbo.cXML WHERE EmailID = #YourEmailID AND ClientID = #YourClientID)
BEGIN
UPDATE dbo.cXML
SET cycleXML = #YourXML
WHERE EmailID = #YourEmailID AND ClientID = #YourClientID
END
ELSE BEGIN
INSERT INTO dbo.cXML (EmailID, ClientID, cycleXML)
SELECT #YourEmailID, #YourClientID, #YourXML
END