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" />

con la seguente:

<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 😀