Ci sono sempre mille pensieri ogni volta che si vuole aggiungere un pagamento sul proprio sito o quello di un cliente tramite PayPal e alla fine la scelta più opportuna ricade sui pulsanti preimpostati di PayPal.
Quello che molti non sanno è che quei pulsanti creano un semplice pagamento e non fanno sapere al vostro sito se il pagamento è stato effettuato correttamente.
Per far si che dopo un pagamento il vostro sito sappia chi e quale servizio ha pagato dovrete usare il metodo IPN (instant payment notification).
Questo sistema è molto semplice se si capisce da subito il funzionamento. Avremo bisogno di:
– Un form contenenti i dati e le pagine in caso di annullamento di pagamento o buona riuscita
– Una pagina che rimane in ascolto per ogni azione fatta dall’utente in PayPal (questa pagina sarà quella da mettere nel form sotto la voce notify_url)
In questo modo potremo sapere esattamente quando un utente ha pagato e quale servizio a comprato, così da modificare magari un campo nel database e attivargli un servizio piuttosto che sbloccargli qualche funzione o aggiungergli “gettoni” in caso di giochi o aste al ribasso.
Come al solito la faccio breve. Ecco a voi i codici da usare. Il primo andrà incorporato nella vostra pagina poiché è il pulsante di pagamento (ovviamente dovrete inserire le vostre informazioni) mentre il secondo codice è LA pagina che rimarrà in ascolto, quindi copiate il codice in toto e incollatelo in una pagina vuota, salvatelo e siete a posto (consiglio di creare un file per ogni servizio che offrite, ovvero uno per i gettoni e uno per l’acquisto di una maglietta ad esempio).
Codice da implementare come pulsante pagamento:
<form method="post" name="paypal_form" action="https://www.sandbox.paypal.com/cgi-bin/webscr"> <input type="hidden" name="business" value="email.venditore@suoprovider.it" /> <input type="hidden" name="cmd" value="_xclick" /> <!-- informazioni sulla transazione --> <input type="hidden" name="return" value="<?php echo "http://".$_SERVER['HTTP_HOST']; ?>/shop/conferma_pagamento.php" /> <input type="hidden" name="cancel_return" value="<?php echo "http://".$_SERVER['HTTP_HOST']; ?>/shop/cancel.php" /> <input type="hidden" name="notify_url" value="<?php echo "http://".$_SERVER['HTTP_HOST']; ?>/shop/ipn.php" /> <input type="hidden" name="rm" value="2" /> <input type="hidden" name="currency_code" value="EUR" /> <input type="hidden" name="lc" value="IT" /> <input type="hidden" name="cbt" value="Continua" /> <!-- informazioni sul pagamento --> <input type="hidden" name="shipping" value="7.00" /> <input type="hidden" name="cs" value="1" /> <!-- informazioni sul prodotto --> <input type="hidden" name="item_name" value="Guida PayPal IPN con PHP" /> <input type="hidden" name="amount" value="100.00" /> <!-- questo campo conterrà le info che torneranno al sito e viene usato per passare l'id utente o altre info --> <input type="hidden" name="custom" value="ABR24" /> <!-- informazioni sull'acquirente --> <input type="text" name="first_name" /> <input type="text" name="last_name" /> <input type="text" name="address1" /> <input type="text" name="city" /> <input type="text" name="state" /> <input type="text" name="zip" /> <input type="text" name="email" /> <!-- pulsante pagamento --> <input type="image" src="http://www.paypal.com/it_IT/i/btn/x-click-but01.gif" border="0" name="submit" alt="Paga subito con PayPal" /> </form>
Questo articolo vuole essere un rapido articolo per chi conosce bene il php ma se siete svegli potete capirlo da soli dove mettere le mani ;)Qui invece trovate il codice della pagina intera. Copiatelo per intero in una pagina vuota e salvatelo così com’è. L’unica cosa che dovrete andare a cambiare è l’azione che viene fatta nel database. Consiglio di chiamare questa pagina “ipn.php” e sarà la pagina da inserire nel form precedente sotto la voce “notify_url”! (molto importante e da ricordare, se no non funziona)
<?php // intercetta le variabili IPN inviate da PayPal $req = 'cmd=_notify-validate'; // legge l'intero contenuto dell'array POST foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; } // intestazione, prepara le variabili PayPal per la validazione $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; // apre una connessione al socket PayPal $fp = fsockopen ('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30); // converte le variabili inviate da IPN in variabili locali $txn_id = filter_var($_POST['txn_id'], FILTER_SANITIZE_STRING); $payment_status = filter_var($_POST['payment_status'], FILTER_SANITIZE_STRING); $receiver_email = filter_var($_POST['receiver_email'], FILTER_SANITIZE_EMAIL); $payer_email = filter_var($_POST['payer_email'], FILTER_SANITIZE_EMAIL); $first_name = filter_var($_POST['first_name'], FILTER_SANITIZE_STRING); $last_name = filter_var($_POST['last_name'], FILTER_SANITIZE_STRING); $address_street = filter_var($_POST['address_street'], FILTER_SANITIZE_STRING); $address_city = filter_var($_POST['address_city'], FILTER_SANITIZE_STRING); $address_state = filter_var($_POST['address_state'], FILTER_SANITIZE_STRING); $address_zip = filter_var($_POST['address_zip'], FILTER_SANITIZE_STRING); // verifica l'apertura della connessione al socket if (!$fp) { // se la connessione non avviene l'esecuzione dello script viene bloccata exit(); // in alternativa è per esempio possibile inviare un'email al venditore } else { // elaborazione delle informazioni fputs ($fp, $header . $req); while (!feof($fp)) { $res = fgets ($fp, 1024); // azioni in caso di risposta positiva da parte di PayPal if (strcmp ($res, "VERIFIED") == 0) { // controllo sull'email del venditore if($receiver_email == " email.venditore@suoprovider.it"){ // connessione a MySQL tramite istanza $mysqli = new mysqli("localhost", "username", "password", "ordini"); $count = $mysqli->query("SELECT id_ordini FROM notifiche WHERE txn_id = '$txn_id'"); // controllo sull'identificatore della transazione if ($mysqli->affected_rows == 0){ // query per l'inserimento dei dati $result = $mysqli->query("INSERT INTO notifiche (txn_id, payment_status, payer_email, first_name, last_name, address_street, address_city, address_state, address_zip) VALUES ('$txn_id', '$payment_status', '$payer_email', '$first_name', '$last_name', '$address_street', '$address_city', '$address_state', '$address_zip')"); } // liberazione della memoria dal risultato della query $count->close(); // chiusura della connessione $mysqli->close(); } } // azione in caso di risposta negativa da parte di PayPal else if (strcmp ($res, "INVALID") == 0) { // è possibile eseguire qualsiasi operazione // per esempio compilare un log degli errori o inviare una mail al venditore } } // chiusura della sorgente di dati fclose($fp); } ?>
Spero sia chiaro come fare il tutto, nel caso ci sono i commenti ad ogni riga.
I codici sono tratti da un articolo simile al mio ma molto più esplicativo che potete trovare qui. ma come ho detto questo vuole essere un rapido articolo per rinfrescare la memoria a chi usa poco queste funzioni direi indispensabili quando ci si trova a doverle implementare ma che non sempre sono richieste dai clienti 🙂
Buon lavoro!
P.S. – I link di paypal nei codici sono per la sandbox, quindi l’area prova per i test di pagamento, di paypal. Per puntare al sito di pagamento vero e proprio togliete la parola “sandbox” lasciando così http://www.paypal.com ecc…
P.S.BIS – Se volete mettere un menù a tendina con più opzioni di prezzo, vi basterà sostituire la riga:
<input type="hidden" name="amount" value="100.00" />
<table> <tr><td><input type="hidden" name="on0" value="Durata">Durata</td></tr><tr><td><select name="os0"> <option value="1 Settimana">1 Settimana €1,00 EUR</option> <option value="1 Mese">1 Mese €2,50 EUR</option> <option value="3 Mesi">3 Mesi €6,00 EUR</option> </select> </td></tr> </table> <input type="hidden" name="option_select0" value="1 Settimana"> <input type="hidden" name="option_amount0" value="1.00"> <input type="hidden" name="option_select1" value="1 Mese"> <input type="hidden" name="option_amount1" value="2.50"> <input type="hidden" name="option_select2" value="3 Mesi"> <input type="hidden" name="option_amount2" value="6.00"> <input type="hidden" name="option_index" value="0">
Ovviamente sostituite i dati con i vostri 😉
ALTRA SOLUZIONE!
Ho scoperto che con PHP 5 non sempre lo script IPN sopra citato funziona, quindi suggerisco quello di Micah Carrick che si può riassumere in un singolo file anche se un pochino più lungo del precedente (volendo potete dividere i file come mostra lui stesso nella sua versione base, unita da me per comodità il tutto nel file IPN).
<?php /** * PayPal IPN Listener * * A class to listen for and handle Instant Payment Notifications (IPN) from * the PayPal server. * * https://github.com/Quixotix/PHP-PayPal-IPN * * @package PHP-PayPal-IPN * @author Micah Carrick * @copyright (c) 2012 – Micah Carrick * @version 2.1.0 */ class IpnListener { /** * If true, the recommended cURL PHP library is used to send the post back * to PayPal. If flase then fsockopen() is used. Default true. * * @var boolean */ public $use_curl = true; /** * If true, explicitly sets cURL to use SSL version 3. Use this if cURL * is compiled with GnuTLS SSL. * * @var boolean */ public $force_ssl_v3 = true; /** * If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any * “Location: …” headers in the response. * * @var boolean */ public $follow_location = false; /** * If true, an SSL secure connection (port 443) is used for the post back * as recommended by PayPal. If false, a standard HTTP (port 80) connection * is used. Default true. * * @var boolean */ public $use_ssl = true; /** * If true, the paypal sandbox URI http://www.sandbox.paypal.com is used for the * post back. If false, the live URI http://www.paypal.com is used. Default false. * * @var boolean */ public $use_sandbox = false; /** * The amount of time, in seconds, to wait for the PayPal server to respond * before timing out. Default 30 seconds. * * @var int */ public $timeout = 30; private $post_data = array(); private $post_uri = ”; private $response_status = ”; private $response = ”; const PAYPAL_HOST = ‘www.paypal.com’; const SANDBOX_HOST = ‘www.sandbox.paypal.com’; /** * Post Back Using cURL * * Sends the post back to PayPal using the cURL library. Called by * the processIpn() method if the use_curl property is true. Throws an * exception if the post fails. Populates the response, response_status, * and post_uri properties on success. * * @param string The post data as a URL encoded string */ protected function curlPost($encoded_data) { if ($this->use_ssl) { $uri = ‘https://’.$this->getPaypalHost().’/cgi-bin/webscr’; $this->post_uri = $uri; } else { $uri = ‘http://’.$this->getPaypalHost().’/cgi-bin/webscr’; $this->post_uri = $uri; } $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__).”/cert/api_cert_chain.crt”); curl_setopt($ch, CURLOPT_URL, $uri); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_data); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $this->follow_location); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); if ($this->force_ssl_v3) { curl_setopt($ch, CURLOPT_SSLVERSION, 3); } $this->response = curl_exec($ch); $this->response_status = strval(curl_getinfo($ch, CURLINFO_HTTP_CODE)); if ($this->response === false || $this->response_status == ’0′) { $errno = curl_errno($ch); $errstr = curl_error($ch); throw new Exception(“cURL error: [$errno] $errstr”); } } /** * Post Back Using fsockopen() * * Sends the post back to PayPal using the fsockopen() function. Called by * the processIpn() method if the use_curl property is false. Throws an * exception if the post fails. Populates the response, response_status, * and post_uri properties on success. * * @param string The post data as a URL encoded string */ protected function fsockPost($encoded_data) { if ($this->use_ssl) { $uri = ‘ssl://’.$this->getPaypalHost(); $port = ’443′; $this->post_uri = $uri.’/cgi-bin/webscr’; } else { $uri = $this->getPaypalHost(); // no “http://” in call to fsockopen() $port = ’80′; $this->post_uri = ‘http://’.$uri.’/cgi-bin/webscr’; } $fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout); if (!$fp) { // fsockopen error throw new Exception(“fsockopen error: [$errno] $errstr”); } $header = “POST /cgi-bin/webscr HTTP/1.1\r\n”; $header .= “Host: “.$this->getPaypalHost().”\r\n”; $header .= “Content-Type: application/x-www-form-urlencoded\r\n”; $header .= “Content-Length: “.strlen($encoded_data).”\r\n”; $header .= “Connection: Close\r\n\r\n”; fputs($fp, $header.$encoded_data.”\r\n\r\n”); while(!feof($fp)) { if (empty($this->response)) { // extract HTTP status from first line $this->response .= $status = fgets($fp, 1024); $this->response_status = trim(substr($status, 9, 4)); } else { $this->response .= fgets($fp, 1024); } } fclose($fp); } private function getPaypalHost() { if ($this->use_sandbox) return self::SANDBOX_HOST; else return self::PAYPAL_HOST; } /** * Get POST URI * * Returns the URI that was used to send the post back to PayPal. This can * be useful for troubleshooting connection problems. The default URI * would be “ssl://www.sandbox.paypal.com:443/cgi-bin/webscr” * * @return string */ public function getPostUri() { return $this->post_uri; } /** * Get Response * * Returns the entire response from PayPal as a string including all the * HTTP headers. * * @return string */ public function getResponse() { return $this->response; } /** * Get Response Status * * Returns the HTTP response status code from PayPal. This should be “200″ * if the post back was successful. * * @return string */ public function getResponseStatus() { return $this->response_status; } /** * Get Text Report * * Returns a report of the IPN transaction in plain text format. This is * useful in emails to order processors and system administrators. Override * this method in your own class to customize the report. * * @return string */ public function getTextReport() { $r = ”; // date and POST url for ($i=0; $i<80; $i++) { $r .= ‘-’; } $r .= “\n[".date('m/d/Y g:i A').'] – ‘.$this->getPostUri(); if ($this->use_curl) $r .= ” (curl)\n”; else $r .= ” (fsockopen)\n”; // HTTP Response for ($i=0; $i<80; $i++) { $r .= ‘-’; } $r .= “\n{$this->getResponse()}\n”; // POST vars for ($i=0; $i<80; $i++) { $r .= ‘-’; } $r .= “\n”; foreach ($this->post_data as $key => $value) { $r .= str_pad($key, 25).”$value\n”; } $r .= “\n\n”; return $r; } /** * Process IPN * * Handles the IPN post back to PayPal and parsing the response. Call this * method from your IPN listener script. Returns true if the response came * back as “VERIFIED”, false if the response came back “INVALID”, and * throws an exception if there is an error. * * @param array * * @return boolean */ public function processIpn($post_data=null) { $encoded_data = ‘cmd=_notify-validate’; if ($post_data === null) { // use raw POST data if (!empty($_POST)) { $this->post_data = $_POST; $encoded_data .= ‘&’.file_get_contents(‘php://input’); } else { throw new Exception(“No POST data found.”); } } else { // use provided data array $this->post_data = $post_data; foreach ($this->post_data as $key => $value) { $encoded_data .= “&$key=”.urlencode($value); } } if ($this->use_curl) $this->curlPost($encoded_data); else $this->fsockPost($encoded_data); if (strpos($this->response_status, ’200′) === false) { throw new Exception(“Invalid response status: “.$this->response_status); } if (strpos($this->response, “VERIFIED”) !== false) { return true; } elseif (strpos($this->response, “INVALID”) !== false) { return false; } else { throw new Exception(“Unexpected response from PayPal.”); } } /** * Require Post Method * * Throws an exception and sets a HTTP 405 response header if the request * method was not POST. */ public function requirePostMethod() { // require POST requests if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != ‘POST’) { header(‘Allow: POST’, true, 405); throw new Exception(“Invalid HTTP request method.”); } } } ?> <?php //INIZIO SECONDA PARTE. POTETE SEPARARLA E TENERE IL CODICE PRECEDENTE IN UN SINGOLO FILE “ipnlistener.php” E METTERE QUESTA PARTE NEL FILE “ipn.php” $custom=$_POST['custom']; // il valore custom settato nel form iniziale, qui lo riprendiamo per utilizzarlo // include(‘ipnlistener.php’); // Questo nel caso mettiate il file all’esterno $listener = new IpnListener(); $listener->use_sandbox = true; try { $verified = $listener->processIpn(); } catch (Exception $e) { // fatal error trying to process IPN. exit(0); } if ($verified) { // IPN dà l’ok e risponde “VERIFIED” //QUI SI METTERA’ TUTTO IL CODICE NECESSARIO PER L’ATTIVAZIONE DI UN SERVIZIO (db,mail e quant’altro) } else { // IPN dà errore e risponde “INVALID” // Qui l’azione nel caso la transazione abbia avuto qualche problema. } ?>
Trovate il codice direttamente sul sito dello sviluppatore (qui), come sempre questi codici vogliono essere un “ah è vero, era questo il codice” per i dimentichini come me 😀
ho provato ad usare entrambi i modi, ma purtroppo mi da sempre INVALID.
Non puoi darmi una mano gentilmente ?
Hai verificato che tutti i dati passati siano validi? L’account sandbox è stato fatto correttamente? Non è una cosa facilissima, infatti ti consiglio di cercare qualche classe che fa tutto in automatico già impostata 🙂
Ma come faccio a passare nel secondo esempio il valore selezionato nella listbox a ipn.php? Sembra che l’unico valore che passi sia il custom e non si capisce come caricare l’oggetto scelto dalla listbox. Il primo esempio sembra essere più dettagliato, ma purtroppo non funziona.
Ciao. Da quando ho scritto questo post non ho avuto modo di riprovare il codice ma se non ricordo male il secondo esempio va a sostituirsi all’IPN del primo. I valori vengono passati tramite un semplice form che invierà i dati a paypal. Il file IPN fa da tramite tra il sito e paypal (inviandolo a paypal come notify_url direttamente dal form). Grazie all’IPN il sito saprà cosa sta succedendo su paypal e quindi potrà creare o annullare operazioni su eventuali database del sito e/o invio email di conferme. Sarà paypal a comunicare con quel file.
Se hai la necessità di recuperare altri valori oltre al custom penso ti basti recuperare tutti i $_POST in arrivo da paypal e decidere quale ti interessa.
Tu non dovrai puntare il tuo form all’IPN, lo script IPN deve rimanere intaccato, irraggiungibile e non calcolato se non nelle ultime righe dove modificherai le righe (ci sono i commenti) per elaborare i pagamenti sul sito.
Ciao a te. Questo l’avevo capito grazie al tuo interessante articolo. Ora provo a scaricarmi tutti i post per vedere cosa arriva, strano tuttavia che non arrivi la select. L’ultima tua frase non la ho molto capita. Ovviamente lo ho messo nel campo notify_url. Come anche scritto sotto mi va purtroppo anche in crash per poter fare alcunchè con i dati ricevuti.
Inoltre mi becco:
fatal error cURL error: [60] error setting certificate verify locations:
CAfile: /iPhone/inarrivo/paypal/cert/api_cert_chain.crt
CApath: none
Quando chiamo $listener->processIpn()
Per usare questa libreria devi avere installata la libreria cURL nel tuo spazio web. L’errore sembra di permessi: rendi la cartella /iPhone/inarrivo/paypal/cert leggibile
La libreria curl è decisamente installata, fino a poco tempo fa facevo scarpini a sangue di pagine Web! Viceversa non conosco i diritti per la nuova directory creata. Non avendo accesso telnet devo vedere come controllare, altrimenti spostò il tutto sulla mia Vps in USA dove non ho però ancora installato https. Ti faccio sapere se mi si risolve.
A dirti la verità ho anche controllato online e le soluzioni trovate avvalorano la mia tesi: devi dare i permessi di lettura a quella directory. Ho un VPS in Francia e con ubuntu fai tutto anche tramite FTP, cosa che ti consiglio comunque di provare ora se hai accesso a quella directory (anche se non penso).
Se non hai dati sensibili dagli un 767 o per fugare ogni dubbio un 777 e fai i test, poi cala fino ai permessi necessari. Se non è quello beh… un VPS è sempre meglio =)
Buon coding 😉
Ho configurato la directory 777, ma continuo a ricevere l’errore:
[03-Jul-2014 21:27:51] fatal error cURL error: [60] error setting certificate verify locations:
CAfile: /iPhone/inarrivo/paypal/cert/api_cert_chain.crt
CApath: none
trying to process IPN.
In effetti sembra che gli manchi piuttosto il CAPath.
Ciao e complimenti per la spiegazione, ho un problema…
se alla stringa volessi aggiungere la possibilità di far decidere al cliente quanti prodotti scegliere come faccio?
Grazie in anticipo
Grazie =)
Quello direi che potresti farlo con un controllo di form (anche in jQuery) per numero massimo di prodotti o direttamente nella pagina (se un utente ha già tot prodotti di quel tipo disabiliti la possibilità di aggiunta del prodotto).
Se hai un esempio da guardare, magari figurato, ti posso dare un consiglio più mirato
Grazie per la risposta, praticamente il sito prevede il tasto pay pal per la vendita di biglietti on line per un concerto e non riesco ad inserire il controllo per l’acquisto di 2 o più biglietti, perché nella pagina pay pal compare sempre quantità 1
Ciao e grazie per il codice. Sto provando il tutto in ambiente di sviluppo, ma il codice non funziona, in fase di pagamento tuto ok, ma l’ipn non esegue alcuna azione nel database e da nessuna parte… anzi, errore 400 bad request se stampi la variabile res. come mai? sai aiutarmi? Te ne sarei grato!!!
Ciao Sebastiano!
Verifica che le url delle notifiche puntino al tuo sito. Trova il modo di loggare quando una chiamata è stata ricevuta da una pagina precisa.