We make software for humans. Custom Mac, Windows, iOS and Android solutions in HyperCard, MetaCard, and RunRev LiveCode
Setting up a PayPal "Buy Now" button to work with a LiveCode CGI wasn't easy. It took days of frustration and eventually a good part of the LiveCode mailing list to work it out. The puzzle fell into place when a combination of list posts made me realize that 1) PayPal won't respond unless it gets a "200 OK" status header, and 2) Apache won't send any headers until the script exits, but PayPal needs to get the header sooner than that.
The script that follows is the solution. I couldn't have figured it out without the list's help, so I'm giving it back to the community. You can use this on your website, but please note that I retain the copyright and it can't be resold in any form.
The script could be rewritten for use with LiveCode Server. If anyone does that, let me know and I will link to it here. Note that if you will use this with HyperActive's Zygodact product, you cannot use LC community server because the Zygodact generator is password protected and LC Community Server can't open it. You will need the Commercial version of LC Server, available from your LC account web page.
That's the total communication process. Once your script knows a transaction is verified, it can do whatever it wants with the data.
If PayPal can't contact your server it will resend the purchase information repeatedly for up to 4 days. The CGI script watches for duplicates and logs them, but doesn't act on them if it's already processed the transaction. PayPal will think it hasn't reached you if it doesn't get a "Status: 200 OK" acknowledgement, so if for some reason PayPal starts re-sending the same data, you should find out why. PayPal won't get your responses if port 433 outbound is blocked so check with your ISP if you think that may be the case.
Once your tests work with the simulator, set up a sandbox "business" account and a "customer" account. Log into the business account and make a test button. Put it on an html page and test with it. The button will let you simulate the whole payment process as the "customer." Then log in as your "business" and view the transaction in the sandbox History pane. It lists all the IPN transactions, their current status, any errors received, and other helpful information.
PayPal has good documentation about using the sandbox: https://developer.paypal.com/us/cgi-bin/devscr. Once everything works in the sandbox, you can be fairly confident it will work on your real web site.
Use the "Customize Advanced Features" section at the bottom of the button generator to add an Instant Notification URL. This goes into the "advanced variables" field at the bottom of the page. The URL should be the path to your LiveCode script in the cgi-bin folder on your server, preceded by "notify_url=", like this:
notify_url=http://www.hyperactivesw.com/cgi-bin/paypal.mt
If you want PayPal to return the customer to a "thank you" page after a purchase, then input the URL for that page in the "Take customers to this URL when they finish checkout" field and turn on the checkbox. This URL should be on your main web site somewhere, not in the cgi-bin folder.
I set the URL for "Take customers to this URL when they cancel their checkout" to the same purchase page they left from, but that's up to you.
When you save the button, PayPal will give you the HTML code for it. Create a purchasing page for your site and paste in the button code. This page goes somewhere on your site where visitors can access it (i.e., not in the CGI folder.)
Description | Example URL |
---|---|
A web page with a PayPal button for purchasing | http://www.hyperactivesw.com/stewtest/index.html |
A "thank you" web page that customers see after purchase | http://www.hyperactivesw.com/stewtest/thanks.html |
A text file containing the PayPal CGI script | http://www.hyperactivesw.com/cgi-bin/paypal.mt |
A form letter with merge variables, for customer auto-replies | http://www.hyperactivesw.com/cgi-bin/autoreply.txt |
An empty text file for log entries | http://www.hyperactivesw.com/cgi-bin/paypalLog.txt |
The (Linux) LiveCode/Revolution engine 3.5 or earlier, or the LC 7.x+ runtime/standalone engine (7.x required for 64-bit hosts) | http://www.hyperactivesw.com/cgi-bin/rev |
Versions of LC between 3.5 and 7.0 will not function as CGI engines, so you need to choose either a very early copy or a newer one. If you use LC 7.x or later, install the Runtime/Standalone engine, not the IDE engine, and make sure you've chosen either the 32-bit or 64-bit version compatible with your hosting service. The engine is named "rev" here for historical reasons but you can rename it if you like. Change the first line of the CGI script to match if you do that.
For general info about CGI scripts and LiveCode, there's a tutorial here: http://www.hyperactivesw.com/cgitutorial/index.html. If you've never used a LiveCode CGI before, it's probably a good idea to at least get the basics there. Most of what follows assumes you know a little about it.
The sample CGI script was made for my 5-cent Beef Stew recipe. All the constants at the top must be changed to your own information. If you are working in the PayPal sandbox, some of these constants will be different than the ones for your live web site. For example, your sandbox email and merchant ID are not the same as your real merchant email and ID. The auto-fill items in the sandbox simulator won't be your items unless you manually edit them. Check all the constant values if something doesn't work, and remember to change them when you go live on your real web site.
The two script globals gName
and gSerial
are for use with HyperActive Software's Zygodact serial key generator. They don't necessarily have to be globals if you're using another registration system; they can be script locals or handler locals instead. If you aren't providing a serial key, just omit [[gSerial]]
from your email reply text. You don't need to change the CGI script.
For Zygodact users: This script will work with the Zygodact key generator for your product. Uncomment these two lines in the startup handler: |
start using stack "gen.rev"
... stop using stack "gen.rev" |
Place the gen.rev stack into the cgi-bin folder with 755 permissions. That's all you need to do. You don't need the sample CGI script that ships with Zygodact. |
The CGI script simply logs the purchase data to the "paypalLog.txt" file. You may want the data to go into a database instead. If so, you'll need to work out any changes to the script; I was happy with the text log so I didn't do that. The transaction ID is returned to the main startup handler after checkOrderData
validates it, and PayPal recommends cross-checking its ID with your database. This script doesn't use it, but if you are using a database you'll probably want to store the ID for tracking or reference.
The "paypal.mt" CGI script is a text file placed in the cgi-bin folder. Be sure to set its permissions to 755 and make sure you've saved the file with unix line endings. The first line of the script must be at the very top of the text file with no blank lines before it.
You can download the CGI script as a zipped text file if you don't want to copy it from this page.
#!rev -ui
global gName,gSerial
-- these constants are for PayPal transactions:
constant kPrice = 0.05 -- price in the Buy Now button
constant kItemName = "Beef Stew Test" -- Buy Now button item name, exactly
constant kItemNum = "BS100" -- Buy Now item ID
constant kMerchantID = "AB1CDE23F4G5H" -- your merchant ID
constant kMerchantEmail = "salesteam@hyperactivesw.com" -- your PayPal merchant email
-- these constants are for auto-reply emails:
constant kCarbonAddr = "jlg@hyperactivesw.com" -- your email; the blind carbon
-- and any warning messages are sent here
constant kBackupAddr = "private@hyperactivesw.com" -- a backup email for warning
-- messages; can be the same as the carbonAddr. If so, you'll just
-- get 2 copies.
constant kFromAddr = "HyperActive Software <salesteam@hyperactivesw.com>" -- the "From" the
-- customer sees in the auto-reply email
constant kReplySubj = "Beef Stew Test purchase info" -- the subject of the
-- auto-reply email
on startup
if $REQUEST_METHOD = "POST" then -- this is the only kind PayPal sends
read from stdin until empty
put it into tOrderData
-- this next bit only runs if PayPal resends a duplicate notification you've
-- already processed. We just log the duplicate and bail out. But if you see this,
-- PayPal isn't hearing your responses and you should find out why.
if tOrderData is in url ("file:paypalLog.txt") then
LOG "Repeat notification" && the date && the long time && tOrderData
exit startup -- this will cause Apache to throw a "server 500" error
end if
LOG the date && the long time && "Received:" && tOrderData -- raw PayPal data, urlEncoded
put "cmd=_notify-validate&" before tOrderData -- add required response cmd
if "test_ipn=1" is in tOrderData then -- we're in the sandbox
put "https://www.sandbox.paypal.com/cgi-bin/webscr" into tPPAddr
else -- live website
put "https://www.paypal.com/cgi-bin/webscr" into tPPAddr
end if
put "curl --data" &"e& tOrderData "e&& tPPAddr into tPostCmd
put shell(tPostCmd) into tCurlRslt
put last word of tCurlRslt into tResponse -- "VERIFIED" or "INVALID"; ignore the ascii progress meter
put "Status: 200 OK" &cr&cr -- without this, PP will think the server is unresponsive
put "ok" -- probably superfluous but won't hurt
LOG "Response:" && tResponse
if tResponse = "VERIFIED" then -- data is from PayPal
put checkOrderData(tOrderData) into tData -- verify the details & extract the info we need
LOG "Parsed data:" && tData -- log what we extracted
if tData <> "false" then -- valid data
set the itemDel to "|"
put item 1 of tData into gName -- already urldecoded
put item 2 of tData into tEmail -- customer email address
put item 3 of tData into tTransactionID -- not using it here but databases should
put "Content-Type: text/plain" & cr & cr -- add a header
if gName = "" then
LOG "Error: name is missing."
exit startup
end if
# -- start using stack "gen.rev" -- uncomment for Zygodact key generator;
-- other systems should generate a serial key here and populate the gSerial variable with it
LOG gSerial
sendEmail tEmail
# -- stop using stack "gen.rev" -- for Zygodact generators
end if
else if tResponse = "INVALID" then -- something's wrong, email me:
sendEmail kCarbonAddr,"invalid"
end if
else -- not a POST request; just display a vague error
put "Incorrect request" into buffer
put "Content-Type: text/plain" & cr
put "Content-Length:" && the length of buffer & cr & cr
put buffer
end if
end startup
function checkOrderData pData -- ensure that the order should be processed;
-- if so, return the 3 items we need. PayPal sends the data URLEncoded.
put urlDecode(pData) into pData
split pData by "&" and "="
if pData["receiver_email"] <> kMerchantEmail then return false -- not for me
if pData["receiver_id"] <> kMerchantID then return false -- not my PP account
if pData["payment_status"] <> "Completed" then return false -- don't process pending orders
if pData["txn_type"] <> "web_accept" then return false -- not a Buy Now button
if (pData["first_name"] = "" and pData["last_name"] ="") and \
(pData["payer_business_name"] = "") then return false -- no name provided
if pData["mc_gross"] < kPrice then return false -- underpaid; foreign funds are returned in local currency so it's okay to check the USD amount here
if pData["item_name"] <> kItemName then return false -- not my item
if pData["item_number"] <> kItemNum then return false -- not my item number
-- data ok; get user info:
put pData["first_name"] && pData["last_name"] into tName
if tName = "" then put pData["payer_business_name"] into tName
put pData["payer_email"] into tEmail
put pData["txn_id"] into tTransactionID -- PayPal ID for this payment
put "|" into tDel
return tName &tDel& tEmail &tDel& tTransactionID
end checkOrderData
--### email handlers (thank you Andre!)
on sendEmail pEmailAddr,pType
if pType = "invalid" then -- notify me of spoof attempt and bcc a backup email
put "Invalid purchase attempt" into tSubj
put "Invalid response was received from Paypal. Check CGI log." into tMsg
put kBackupAddr into tBcc
else -- email to customer & blind carbon me
put kReplySubj into tSubj
put url ("file:autoreply.txt") into tReplyTxt
put merge(tReplyTxt) into tMsg
put kCarbonAddr into tBcc
end if
mail pEmailAddr,tSubj,tMsg,kFromAddr,tBcc
end sendEmail
function shellEscape pText
repeat for each char tChar in "\`$" & quote -- original included "!" but it interfered with body text
replace tChar with "\" & tChar in pText
end repeat
return pText
end shellEscape
function wrapQ pText -- wrap quotes around text
return quote & pText & quote
end wrapQ
on mail pTo,pSub,pMsg,pFrom,pBcc,pCc -- send email
put "Copy of mail sent:" &cr&cr & pMsg into tBCCMsg
put wrapQ(shellEscape(pTo)) into pTo
put wrapQ(shellEscape(pSub)) into pSub
put wrapQ(shellEscape(pMsg)) into pMsg
put wrapQ(shellEscape(tBCCMsg)) into tBCCMsg
put wrapQ(shellEscape(pFrom)) into pFrom
put wrapQ(shellEscape(pBcc)) into pBcc
# put wrapQ(shellEscape(pCC)) into pCc
get shell("echo -e" && pMsg && "| mail -s" && pSub && pTo && "-- -f" && pFrom)
get shell("echo -e" && tBCCMsg && "| mail -s" && pSub && pBcc && "-- -f" && pFrom) -- bcc sent separately
put it
end mail
--### Logging:
on LOG pData
put "paypalLog.txt" into tFile
open file tFile for append
write pData &cr & cr to file tFile
close file tFile
end LOG
The email template should contain variables for the customer name and serial key using LiveCode merge notation. The two variables gName
and gSerial
are enclosed in double square brackets, which allows LiveCode's merge
command to substitute the values of those variables when it creates the email. If you aren't using a serial key, just omit the gSerial
reference from the template. Here's a sample reply mail:
Dear [[gName]], Thanks for your purchase! You can download Jacque's Beef Stew recipe here: http://www.hyperactivesw.com/stewtest/Jacque's%20Stew.pdf If you ever feel the need to license your food, use this: Username: [[gName]] Serial: [[gSerial]] Thanks again. I think you'll like the stew. Jacqueline Landman Gay -- Jacqueline Landman Gay | dishwasher@hyperactivesw.com HyperActive Software | http://www.hyperactivesw.com
Save the auto-reply text file (with unix line endings) as "autoreply.txt", copy it to the cgi-bin folder, and set its permissions to 755.