Elevating User Account Control with Delphi

If you have medium size of Delphi based application that required some privileges access to Windows OS particularly, then you might face problem with User Account Control (UAC). User Account Control (UAC) is a technology and security infrastructure introduced since Microsoft's Windows Vista. It aims to improve the security of Windows by limiting application software to standard user privileges until an administrator authorizes an increase or elevation. However without privileges access, application that need access to system folder or database might impact to failure.

This time lesson will share you 2 alternatives how the application "playing" with UAC. The first option order user to elevating the authorization manually by executing application using Run As Administrator from right click context menu of the EXE binary. While last option inject an embedding code to elevating user automatically. Both alternatives options came with the same scenario, a login form with Login button enabled if user has full privileges access, and so on in contrary. Let the learn begin, and here the detail explanation:

1. Detecting Elevation Level with ElevationType
We know that UAC only affect with OS greater than Windows XP, so you need also to detect what kind of OS version where the application currently running on. In order so, put below function on the code:

function osver: string;
begin
result := 'Unknown (Windows ' + IntToStr(Win32MajorVersion) + '.' + IntToStr(Win32MinorVersion) + ')';
case Win32MajorVersion of
        4:
                case Win32MinorVersion of
                        0: result := 'W95';
                        10: result := 'W98';
                        90: result := 'WME';
                end;
        5:
                case Win32MinorVersion of
                        0: result := 'W2K';
                        1: result := 'WXP';
                end;
        6:
                case Win32MinorVersion of
                        0: result := 'WVista';
                        1: result := 'W7';
                end;
end;
end;

Then, you can put elevation user account detector on form onShow event just like code below :

procedure TForm1.FormShow(Sender: TObject);
const
TokenElevationType = 18;
TokenElevation     = 20;
TokenElevationTypeDefault = 1;
TokenElevationTypeFull    = 2;
TokenElevationTypeLimited = 3;

var
token: Cardinal;
ElevationType: Integer;
Elevation: DWord;
dwSize: Cardinal;
RunAsAdministrator:boolean;
level: string;
begin
SB.Panels[0].Text:='OS type: ' + osver;

// Run As Administrator Validator
RunAsAdministrator:=false;
if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, token) then
        try
        if GetTokenInformation(token, TTokenInformationClass(TokenElevationType), @ElevationType, SizeOf(ElevationType), dwSize) then
                case ElevationType of
                        TokenElevationTypeDefault:  RunAsAdministrator:=false;
                        TokenElevationTypeFull:     RunAsAdministrator:=true;
                        TokenElevationTypeLimited:  RunAsAdministrator:=false;
                else
                        RunAsAdministrator:=false;
                end
        else if (osver='W95') or (osver='W98') or (osver='WXP') then RunAsAdministrator:=true;

        if GetTokenInformation(token, TTokenInformationClass(TokenElevation), @Elevation, SizeOf(Elevation), dwSize) then
                begin
                if Elevation = 0 then RunAsAdministrator:=false
                else RunAsAdministrator:=true;
                end
        else if (osver='W95') or (osver='W98') or (osver='WXP') then RunAsAdministrator:=true;

        finally
        CloseHandle(token);
        end;

level:='(Administrator)';
btnLogin.Enabled:=true;
if not RunAsAdministrator then
        begin
        level:='(NOT Administrator)';
        btnLogin.Enabled:=false;
        end;
SB.Panels[0].Text:=SB.Panels[0].Text + ' ' + level;
end;

That's all. Try to execute the application in Windows 7, here's what I've got:



The Login button disabled since the application not executed with Run As Administrator context menu.



But, if the application running from Run As Administrator, the Login button now enabled!



Note that above code will not impact on Windows XP, since OS assumed that user running it as Administrator account even it executed from context menu or not.



2. Elevate Execution Level by Manifest file
Create a .manifest file :



Save it as .manifest. Continue to create a .rc file and named it as .rc.



Ah... don't forget to save the .manifest and .rc file in the same folder with the application source code. Then, continue to open command prompt, switch to directory where brcc32.exe (Borland Resource Compiler) exist and gives below syntax:



After the syntax succesfully compiled, it will automatically created a .res and .rec.



Back to the application source code and move to Project Manager window, right click to Project Files and point to View Source menu.



On .DPR menu, gives a line syntax after $R compiler directive just like picture below:



Finished! You may test the application and see what the different between options no #1 and #2.



Have a great fuckin' day!

Labels: ,

  Post a Comment

(Free) Distributed System Proof-of-Concept

I recently completed and implementing the system design of an application. The system design is web based, but the direction shifted to win32 desktop-based application in the second half of the design phase. This isn't ideal, but it's still fine, as system design phase mainly focuses on functional module design, database design and capturing business requirements from a more technical view.

Lots of facts happened while I running the system, but only several major things that I could tell you right now. It seems to be an early warning for you if you have the same project type as me, such as :

1. Database synchronization
This internal application has local database containing tens of tables that should synchronized to the web system when it ready. Now you need to learn, when and how the database will transported down or up - or even control what's down to user database. Beware that weak system design will make synchronization potentially ruined.

2. User Sign On
User control is more important to the system wide globally that affect both desktop and web after all. Notice when and how is user accepted or rejected from the system.

3. Application Distribution
At this moment, the biggest benefit everyone agrees, is that web-application does not require client side deployment. But how you control desktop based application deployment and versioning problem more effective?

However, we're focussed at last point, the software distribution it self. Since this particular system is unique, but I think I made this one succesfully. Starting from theory lesson, ends up with the conceptual practice, finally I named it as SISPIu Distribution Management. And here's the story...

Theory
Software distribution is the process of delivering software to the end user. It may take the form of a binary distribution, with an executable or installer which can be downloaded from the network or wide area Internet.



At least, there are four features of software distribution system. One of it, is, remote installation, which it will perform automatic installation and execution of software as well as wizard utility to make a distribution package. Other than that is monitoring status of distribution, it provided with compression technique to minimize size of files being distribute and it will be function to remotely some operations (reboot PC and remote execution if it necessary). Additional Features that included is real time monitoring of software distribution process. Enable or disable limit administrative function to Normal User and standardize Desktop
Environment are the roles of policy control.

Basic Principal of SISPIu Distribution Management
For the sake of completeness, I’ve included basic principal Guidelines below. It's slightly designed to suit the needs, there are:
  1. Local installation contains package and updater module. While the updater module functioned as package transporter, the package it self should meet versioning number rule below.
  2. A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers. X is the year age of software (starting with 1), Y is the month age of software after X, and Z is the day of software released. Each element can bound numerically and depends on when the package deployed. For instance: 1.0.0 (initial deployment) -> 1.9.5 -> 1.10.9 -> 1.11.24. Patch version is not considerably needed, but you able to add it. Hence, there's no 0 number here (except for initial version). Nor major or minor version. This is what I called as simplify the version number, as we'll know directly how long that two version released or how old the application stand?
  3. Once a versioned package has been released, the contents of that version MUST NOT be modified. Any modifications must be released as a new version. All packages (old and new version) must stored in the same server directory and accessible through defined IP number and port, which that both number (also username and password if required) are embedded to the package.
  4. The package should contains versioning checker and validation module, also server URL and access account. Once a versioned package detected as a newest than existing package version, call the updater module and shut the current running package.
  5. After download successfully the new package, updater module should execute new package and shut down it self.
That's all, but anyway, sorry, I can't share code for this concept :)

Conclusion
I rely on various web applications to do tasks, projects and life. But I still believe the future of computing isn't entirely web-based. It's necessary to have the desktop as the pivotal point, because the power of the desktop is important for a rich user experience - and will be, for a very long time to come.

That is why software distribution become my critical point since what we require then are smart, webified, internet deployable desktop applications - that can reliably store data, serve it robustly, and interact with both remote and local databases. Also uninterrupted experience during version updates.

What is your thought? Do you have a successful story or failure to share?

Labels: ,

  Post a Comment

Delphi: Dragging Object Using TListBox

For some reasons, drag and drop operation is considered to be an important construction approach in many particular applications. The objective is to make user more friendly to the application, by chunking together 2 operands into a single action. However, this operation requires more physical effort than moving the same pointing device without holding down any buttons. Because some users can't move as quickly and precisely while dragging (see Fitts' law) .

Anyway, this drag and drop operation finally used for M8 framework generator application.



The basic ideas is moving object from source to target (both TListBox objects), while object from source (TListBox A) can dragged horizontally into target (TListBox B), but the objects contained in TListBox B also able to dragged vertically.



To make objects able to drag horizontally from TListBox A into TListBox B, there should be onMouseDown trigger code in TListBox A and onTargetDragOver in TListBox B. Here's below the code:

procedure TFUtama.ListUISourceMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
dragUnder:=false;
ListUISource.BeginDrag(True);
end;

procedure TFUtama.ListUITargetDragOver(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: Boolean);
begin
Accept := (Source is TListBox);
end;



To enable objects contained in TListBox B dragged vertically, use onMouseDown and onDragDrop event in TListBox B. Here's below the code:

procedure TFUtama.ListUITargetMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
StartingPoint.X := X;
StartingPoint.Y := Y;
dragUnder:=true;
end;

procedure TFUtama.ListUITargetDragDrop(Sender, Source: TObject; X,
  Y: Integer);
var
 i,j,k : Integer;
 str : string;
 DropPosition, StartPosition: Integer;
 DropPoint: TPoint;
begin
DropPoint.X := X;
DropPoint.Y := Y;
with Source as TListBox do
 begin
 for i := 0 to Items.Count - 1 do
  if (Selected[i] and not dragUnder) then (Sender AS TListBox).Items.AddObject(Items[i], Items.Objects[i])
  else if (Selected[i] and dragUnder) then
   begin
   StartPosition := ItemAtPos(StartingPoint,True) ;
   DropPosition := ItemAtPos(DropPoint,True) ;
   Items.Move(StartPosition, DropPosition) ;
     
   if StartPosition>DropPosition then // 6>1
    begin
    for j:= StartPosition-1 downto DropPosition do
     begin
     // do some operation while StartPosition > DropPosition
     end;
    end
   else
    begin
    for j:= StartPosition+1 to DropPosition do
     begin
     // do some operation while StartPosition < DropPosition
     end;
    end;
   end;
 dragUnder:=false;
 end;
end;



Conclusion:
Use this operations at your own risk! :)

Labels:

  Post a Comment

Explode Function in Delphi

What do you know about "explode" in programming language?  Yes, it defines an additional set of parser functions that operate on strings. In some programming languages, there's native explode function found on their library - except for Delphi that didn't had it yet. So, most of Delphi programmers should build it manually. Anyway, I've been made a mini researches for manual Explode code in Delphi and need to find out what will work best - more effective and efficient.

There's 2 types of it, one with TArray constructor and others with TStrings constructor. Take a look at both codes below:

Explode with TArray

function Explode1(cDelimiter,  sValue : string; iCount : integer) : TArray;
var s : string; i,p : integer;
begin
s := sValue; i := 0;
while length(s) > 0 do
        begin
        inc(i);
        SetLength(result, i);
        p := pos(cDelimiter,s);
        if ( p > 0 ) and ( ( i < iCount ) OR ( iCount = 0) ) then
                begin
                result[i - 1] := copy(s,0,p-1);
                s := copy(s,p + length(cDelimiter),length(s));
                end
        else
                begin
                result[i - 1] := s;
                s :=  '';
                end;
        end;
end;

Explode with TStrings

function Explode2(const str: string; const separator: string): TStrings;
var     n: integer;
        p, q, s: PChar;
        item: string;
begin
Result := TStringList.Create;
try
        p := PChar(str);
        s := PChar(separator);
        n := Length(separator);
        repeat
                q := StrPos(p, s);
                if q = nil then q := StrScan(p, #0);
                SetString(item, p, q - p);
                Result.Add(item);
                p := q + n;
        until q^ = #0;
except
        item := '';
        Result.Free;
        raise;
        end;
end;




Test Scenario

So, here's the scenario: need to generate 10,000 lines of string into TMemo with delimiter at the end of each lines to explode. Each of explode types will read TMemo content and parsing it into TListBox component. To counting elapsed time, I put GetTickCOunt before explode function and after looping (parsing each explode result into TListBox). To give you additional visualization, here's time to generate 10,000 lines.



It takes 13.104 sec for one attempt. Take a look also on both parsing image below that potentially affect to the resource of the computer:



Since elapsed time result will different on each test attempt, so I made 3 times test to make sure that the test resulting in correct way.

Test #1



Test #2



Test #3



Conclusion

From the test, I can say that the best way to explode in Delphi is type #2 with some reasons:
  • Arrayless, means consuming low memory
  • Fastest speed elapsed time


So? Take your choice! :)

Labels:

  Post a Comment

POS System Review: The Simplest, The Lightest and The Cheapest

Anyone can find cheapest Point of Sale (POS) hardware environment recently in this world (including barcode printer, barcode scanner, CPU & display monitor)? Well, I can find it for US$ 493 with all brand new devices! No kidding but here's the truth. All things has passed for future consideration, including lower electricity cost, easier to use barcoding on your store around and simple custom-made software for checkout purposed.

This is without receipts printer since this POS focused on stocking intention. So, receipt coupon for customer is infrequently or even never used, hereby changed with manual hand written.

And, here's the secret:


Zotac ZBOX ID41
WTH is this? Weird branded CPU name born in (nearly) Hong Kong with core business on mini-ITX and mini-PC since… 2006. That's why I choose it apart from it best mixed hardware combination.


This mini PC is thinner but wider than Mac Mini G4.


See link below for detail information:

http://www.zotac.com/index.php?page=shop.product_details&flypage=flypage_images-SRW.tpl&product_id=335&category_id=171&option=com_virtuemart&Itemid=100295&lang=un

Interesting?

Epson LabelWorks LW-400
This is an evolution for Epson to providing a low cost label printer. See the spoiler below:

http://www.epson.com/cgi-bin/Store/jsp/Product.do?sku=C51CB70010

The LW-400 is the minimum series to support barcode printing prior to LW-900.


The packing size slightly larger than Mac Mini G4.


A full qwerty printer at notebook size but more a little thicker. Very handy and ergonomic when hands on and feels light weight too. Just like seeing a BlackBerry with monster size :D


There're also 12mm black-on-white LC sample tape cassette including in the box, with easy way to setting-up on the printer. First press lid button to open the back cap and pull it.


Place the tape in proper way (green box) with outer ribbon attached on small slot (red circle).


Next, close the back cap slowly until it show as normal.


This series operated with dual mode; 6AA battery and power cable. You'll find a power cable included in the box. Make a power connection and press power button.


To switch to barcode printing, press ALT+barcode key as shown as picture above. Then pick one from several built-in barcode font available and specify the size. Sorry, can't tell you what else barcode font built-in included since I'm not an Epson salesman :)


Type-in some digits of numerical code to test. If you like to print copies, press copier button (red circle) several times until it displayed how much copy you want to print out. Ended with print button (green box) to start to print.


This LW-400 series also equipped with manual cutter. So, after print stopped, just press green button in right side to cut-off the paper. Simple and easy.

CipherLab 1070
Finally found a low cost scanner with fairly well-known brand name: CipherLab.


See below spoiler:

http://www.cipherlab.com/catalog.asp?CatID=8&SubcatID=10&ProdID=354

Not too worry, it can read Epson barcode print out result (by using EAN-13 small 5cm size :)


Conclusion
Building recently POS (hardware & software) system much easier than couple years ago. From above specification, there's no need to create an add-in module to print the barcode from application.


It's separate anyway. So it takes shorter time to developing the software. What you need is to mix and match everything. That's it! Thank's for reading and see you on next article...

Labels: , , , , , , , , ,

  Post a Comment

Old Trick (Part 2): Retrieving DBF from Borland Delphi

Couple months ago, I'd wrote about how to retrieving MDB database (MS Access) from Borland Delphi. The same thing if we need to connect to an old-fashioned DBF from Delphi application - the different is: it's more quite simpler.

Here below is an example how to read records from a FILENAME.DBF (containing 2 fields; FIELD1 and FIELD2). Don't forget to provide the DBF file on the same directory where the project will be saved - a CDX index file required depends on the DBF typically records. Reading DBF records as simply as using SQL SELECT query just like usual.

To create this basic project, all we need is only TQuery and TButton. Just put it both on a blank form.



The TQuery will work with DBF in a native way. Here is the code, take an attention on a bold sign below.

procedure TFMain.TButtonClick(Sender: TObject);
var FIELD1,FIELD2 : string;
begin
qDBF1.Close;
qDBF1.DatabaseName:=extractfilepath(extractfilepath(application.ExeName)+'FILENAME.DBF');
qDBF1.SQL.Clear;
qDBF1.SQL.Add('select * from FILENAME');
qDBF1.Open;

qDBF1.First;
repeat
FIELD1:=qDBF1.FieldByName('FIELD1').AsString;
FIELD2:=qDBF1.FieldByName('FIELD2').AsString;

qDBF1.Next;
until qDBF1.Eof;
end;


Anyway, I supposed that manipulating DBF record using TQuery component will have the same way by using INSERT, UPDATE and DELETE clauses. In other words, "Just like usual…" :) Cheers!!!

Labels: ,

  Post a Comment

Old Trick: Retrieving MDB MS Access 97 Files from Borland Delphi

Somehow, I’ve been involved with a project contains dated database file from Ms Access (MDB) and Dbase IV type (DBF). The goal is to converting database and it’s value from MDB into DBF. For this purpose, still I develop the application using Borland Delphi 6.

On this experience, I prepare the form with 3 database component; TADOConnection, TADOQuery (for MDB) and TTable (for DBF)



For first initialization MDB files using a connection string, here below my example how to get it done:

ADOConnection1.ConnectionString:='Provider=Microsoft.Jet.OLEDB.4.0;'+'User ID=Admin;Data Source='+Edit1.Text+';Mode=Share Deny None;'+'Persist Security Info=False;';


While Edit1.Text contains drive name & full path where the MDB file exist on local storage. TADOQuery used to retrieve record using SQL query as usual used on TQuery. But note that the SQL syntax between MDB and others database is not exactly the same. You need to carefully type the correct query or you can test it first from MS Access query window.

After it succeeded, retrieve MDB records using TADOQuery, now turn to DBF migration operation. Sometimes, original DBF record using MDX index file. If there’s no MDX files found, it needs to be force so that the application still can read the DBF files & doing operation within. Here below I create a procedure in order to force to by-passing the need of MDX files:

try
Table1.Close;
Table1.DatabaseName:=extractfilepath(extractfilepath(application.ExeName)+filename+'.DBF');
Table1.TableName:=filename+'.DBF';
Table1.Active:=false;
Table1.EmptyTable;
Table1.Open;
except
on E: EDBEngineError do
if Pos('Index does not exist', E.Message) > 0 then
begin
MessageDlg('MDX file not found.Attempting to Open without Index.', mtWarning, [mbOK], 0);
RemoveMDXByte(extractfilepath(application.ExeName)+filename+'.DBF');
PostMessage(btnProses.Handle, cn_Command, bn_Clicked, 0);
end;
end;

The above procedure will execute twice from the TButton action trigger & forcing nonexistence of MDX file. It’s mean that, after TButton pressed, an error window will show up & try except lines will be executed. Next, DBF reading process will be executed again automatically (PostMessage(btnProses.Handle, cn_Command, bn_Clicked, 0)). Have a great experiment on this!

Labels: ,

  Post a Comment

Win32 Application Online Update (Automatically)

As my promise before, here now I present the last part of updating Win32 application methods. On this experiment, I tried to implement an automatically technique so that it will eliminate user’s response to retrieve an upgrade from online source. Any programming language will do the same technique I offered but for this chance, still I used Borland Delphi for the familiarly reason. The trick – I said – is a basic one: first, the form needs to download a text file containing application version number information from the web, meanwhile at the same time the form also need to load the current application version. Then, both numbers must be compared. If newer version detected, the form have to download the update package. That’s it simple.

To get start, create a blank project and fill the form with 2 TMemo’s (to store both version local & online), TStatusBar (to show information during update process), TProgressBar (to show percentage progress), TTimer (to automatically do the process) and a TButton for dummy button test.



Next, please provide a text file (containing latest version number) and an exe package which is latest application to download into an online web directory (for example: http://www.abc.com/download). For this experiment, I used data.txt and data.exe.



Back to Delphi’s editor and think for the first rule of the form; download data.txt from http://www.abc.com/download/data.txt to the disk of local PC. The question is, is it possible to download an Internet file without using a special "download" component? Fortunately, since Delphi 4, this can be done very easily by using the Windows API function URLDownloadToFile. But, it's a pity that this function is not documented in the Help of the earlier Delphi versions, except in Delphi 2006 and later which is mentioned in the Help documents.

To do the trick, firstly, add "URLMon" to the unit's USES clause, if it's not already in the USES clause:

Uses Windows, Messages, …, URLMon;


Then, use the following simple source code example:

if URLDownloadToFile(nil, PChar(SourceFile), PChar(LocalFile), 0, nil) = 0 then CHECK LATEST VERSION & DOWNLOAD IT IF POSSIBLE
else StatusBar1.Panels[0].Text:='Error downloading';

CHECK LATEST VERSION means that both version number (from local & online) need to be compared. If it detected a latest one, make data.exe download directly. For the download progress - in a real world "critical" application - you should give some feedback to the user about what's happening, such as: disable the button until the end of the download, show progress indicator or show some message in a status bar. If you need to save the contents of http://www.abc.com/download/data.exe - and be able to track the download progress, use the TDownLoadURL Delphi action. While TDownLoadURL is writing to the specified file, it periodically generates an OnDownloadProgress event, so that you can provide users with feedback about the process. For this purpose, add unit “ExtActns” in the uses clause.

Uses Windows, Messages, …, URLMon, ExtActns;

type
TForm1 = class(TForm)

private
{ Private declarations }
procedure URL_OnDownloadProgress
(Sender: TDownLoadURL;
Progress, ProgressMax: Cardinal;
StatusCode: TURLDownloadStatus;
StatusText: String; var Cancel: Boolean) ;

implementation

procedure TForm1.URL_OnDownloadProgress;
begin
ProgressBar1.Max:= ProgressMax;
ProgressBar1.Position:= Progress;
end;


procedure TForm1.Button1Click(Sender: TObject);
with TDownloadURL.Create(self) do
try
URL:= http://www.abc.com/download/'data.exe';
FileName := 'e:\data.exe';
OnDownloadProgress := URL_OnDownloadProgress;
ExecuteTarget(nil) ;
finally
Free;
end;
end;
end;


Ok then, note that rule #2 using button as dummy trigger to download the whole process – from data.txt until data.exe -, so that Button1Click event complete like the following source:

procedure TForm1.Button1Click(Sender: TObject);
var wwwString,SourceFile, LocalFile: string;
begin
// initialization
Button1.Enabled:=false;
Memo1.Clear;
Memo2.Clear;
try
Memo1.Lines.LoadFromFile('c:\data.txt');
except
// if it failed to load data.txt from C:
end;
wwwString:='http://www.abc.com/download/'; // online source
SourceFile := wwwString+'data.txt';
LocalFile := 'c:\data.tmp.txt'; // temporary for data.txt

// try to download data.txt from online source
if URLDownloadToFile(nil, PChar(SourceFile), PChar(LocalFile), 0, nil) = 0 then
begin
Memo2.Lines.LoadFromFile('c:\data.tmp.txt');
// compare both version number (local & online)
if Memo1.Text>=Memo2.Text then StatusBar1.Panels[0].Text:='No update available'
else
begin
Memo2.Lines.SaveToFile('e:\data.txt'); // save latest version information
StatusBar1.Panels[0].Text:='Downloading update ('+Memo2.Text+')...';
// downloading latest application
with TDownloadURL.Create(self) do
try
URL:=wwwString+'data.exe';
FileName := 'c:\data.exe';
OnDownloadProgress := URL_OnDownloadProgress;
ExecuteTarget(nil) ;
finally
Free;
end;
end;
end
else
StatusBar1.Panels[0].Text:='Error downloading ' + SourceFile;
deletefile('c:\data.tmp.txt'); // delete temporary
Button1.Enabled:=true;
end;


Make a try to execute the source & press the button to start the (manual) update.



If nothing goes wrong, for the first time, the application will try to start download the latest application. Wait it until the rest process completed. If it succeeded, you may need to move the download trigger into TTimer. And that’s it, the updater application will work. You can add some improvement to the updater such as a procedure to move data.exe into main program or anything you like after you built the updater like above. Please share your comment below & thanks for your attention on this blog.

Labels: , ,

  Post a Comment