I have a website with a simple contact form. The validation is somewhat minimal because it doesn't go into a database; just an email. The form works as such:
There are 5 fields - 4 of which are required. The submit is disabled until the 4 fields are valid, and then you can submit it. Then everything is validated on the server again, including the recaptcha (which is not validated by me client side). The whole process is done with ajax, and there are multiple tests that must pass on the server side or 4** headers are returned, and the fail callback handler is called.
Everything works like gangbusters on Chrome on the desktop (I haven't tried other browsers, but I can't imagine why they'd be different), but on the iPhone the reCaptcha always validates even if I don't check the box for the test.
In other words: I still have to fill out the four values correctly in order to submit, but if I don't check the box for the reCaptcha, the request still succeeds.
I can post some code if anyone thinks that would be helpful, but it appears that the problem is with the device and not with the code. Does anyone have any insight into this?
Note: The server side is PHP/Apache if this is helpful.
Update: 5/28/2015:
I'm still debugging this, but it seems like Mobile Safari is ignoring my response headers on my iPhone. When I output the response to the page what I get on Desktop for (data,status,xhr) is:
data: my response which at this point just says error or success -> error
status: error
xhr: {'error',400,'error'}
On Mobile safari:
data: error
status: success
xhr: {'error',200,'success'}
So - it seems like it's just ignoring my response headers. I tried explicitly setting {"headers":{"cache-control":"no-cache"}} but to no avail.
Update: 6/3/2015
Per Request, here is the code. This is almost certainly more than you need. It has also become more obtuse because of the changes I've made to try and fix it. Also note that, while it may appear that there are variables that haven't been defined, they (should) have been defined in other files.
The client side
$('#submit').on('click', function(e) {
$(this).parents('form').find('input').each(function() {
$(this).trigger('blur');
})
var $btn = $(this);
$btn = $btn.button('loading');
var dfr = $.Deferred();
if ($(this).attr('disabled') || $(this).hasClass('disabled')) {
e.preventDefault();
e.stopImmediatePropagation();
dfr.reject();
return false;
} else {
var input = $('form').serializeArray();
var obj = {},
j;
$.each(input, function(i, a) {
if (a.name === 'person-name') {
obj.name = a.value;
} else if (a.name === 'company-name') {
obj.company_name = a.value;
} else {
j = a.name.replace(/(g-)(.*)(-response)/g, '$2');
obj[j] = a.value;
}
});
obj.action = 'recaptcha-js';
obj.remoteIp = rc.remoteiP;
rc.data = obj;
var request = $.ajax({
url: rc.ajaxurl,
type: 'post',
data: obj,
headers: {
'cache-control': 'no-cache'
}
});
var success = function(data) {
$btn.data('loadingText', 'Success');
$btn.button('reset');
$('#submit').addClass('btn-success').removeClass('btn-default');
$btn.button('loading');
dfr.resolve(data);
};
var fail = function(data) {
var reason = JSON.parse(data.responseText).reason;
$btn.delay(1000).button('reset');
switch (reason) {
case 'Recaptcha Failed':
case 'Recaptcha Not Checked':
case 'One Or more validator fields not valid or not filled out':
case 'One Or more validator fields is invalid':
// reset recaptcha
if ($('#submit').data('tries')) {
$('#submit').remove();
$('.g-recaptcha').parent().addBack().remove();
myPopover('Your request is invalid. Please reload the page to try again.');
} else {
$('#submit').data('tries', 1);
grecaptcha.reset();
myPopover('One or more of your entries are invalid. Please make corrections and try again.');
}
break;
default:
// reset page
$('#submit').remove();
$('.g-recaptcha').remove();
myPopover('There was a problem with your request. Please reload the page and try again.');
break;
}
dfr.reject(data);
};
request.done(success);
request.fail(fail);
}
The Server:
function _send_email(){
$recaptcha=false;
/* * */
if(isset($_POST['recaptcha'])):
$gRecaptchaResponse=$_POST['recaptcha'];
$remoteIp=isset($_POST['remoteIp']) ? $_POST['remoteIp'] : false;
/* ** */
if(!$remoteIp):
$response=array('status_code'=>'409','reason'=>'remoteIP not set');
echo json_encode($response);
http_response_code(409);
exit();
endif;
/* ** */
/* ** */
if($gRecaptchaResponse==''):
$response=array('status_code'=>'400','reason'=>'Recaptcha Failed');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* ** */
if($recaptcha=recaptcha_test($gRecaptchaResponse,$remoteIp)):
$recaptcha=true;
/* ** */
else:
$response=array('status_code'=>'400','reason'=>'Recaptcha Failed');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* ** */
/* * */
else:
$response=array('status_code'=>'400','reason'=>'Recaptcha Not Checked');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* * */
/* * */
if($recaptcha==1):
$name=isset($_POST['name']) ? $_POST['name'] : false;
$company_name=isset($_POST['company_name']) ? $_POST['company_name'] : false;
$phone=isset($_POST['phone']) ? $_POST['phone'] : false;
$email=isset($_POST['email']) ? $_POST['email'] : false;
/* ** */
if(isset($_POST['questions'])):
$questions=$_POST['questions']=='' ? 1 : $_POST['questions'];
/* *** */
if(!$questions=filter_var($questions,FILTER_SANITIZE_SPECIAL_CHARS)):
$response=array('status_code'=>'400','reason'=>'$questions could not be sanitized');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* *** */
/* ** */
else:
$questions=true;
endif;
/* ** */
/* ** */
if( count( array_filter( array( $name,$company_name,$phone,$email ),"filter_false" ) ) !=4 ):
$response=array('status_code'=>'400','reason'=>'One Or more validator fields not valid or not filled out');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* ** */
$company_name=filter_var($company_name,FILTER_SANITIZE_SPECIAL_CHARS);
$name=filter_var($name,FILTER_SANITIZE_SPECIAL_CHARS);
$phone=preg_replace('/[^0-9+-]/', '', $phone);
$email=filter_var($email,FILTER_VALIDATE_EMAIL);
/* ** */
if($company_name && $recaptcha && $name && $phone && $email && $questions):
$phone_str='Phone: ' . $phone;
$company_str='Company: ' . $company_name;
$email_str='Email String: ' . $email;
$name_str='Name: '.$name;
$questions=$questions==1 ? '' : $questions;
$body="$name_str\r\n\r\n$company_str\r\n\r\n$email_str\r\n\r\n$phone_str\r\n\r\n____________________\r\n\r\n$questions";
$mymail='fake#fake.com';
$headers = array();
$headers[] = "MIME-Version: 1.0";
$headers[] = "Content-type: text/plain; charset=\"utf-8\"";
$headers[] = "From: $email";
$headers[] = "X-Mailer: PHP/" . phpversion();
/* *** */
if(mail('$mymail', 'Information Request from: ' . $name,$body,implode("\r\n",$headers))):
$response=array('status_code'=>'200','reason'=>'Sent !');
echo json_encode($response);
http_response_code(200);
exit();
/* *** */
else:
$response=array('status_code'=>'400','reason'=>'One Or more validator fields is invalid');
echo json_encode($response);
http_response_code(400);
exit();
endif;
/* *** */
endif;
/* ** */
endif;
/* * */
$response=array('status_code'=>'412','reason'=>'There was an unknown error');
echo json_encode($response);
http_response_code(412);
exit();
}
function recaptcha_test($gRecaptchaResponse,$remoteIp){
$secret=$itsasecret; //removed for security;
require TEMPLATE_DIR . '/includes/lib/recaptcha/src/autoload.php';
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
$resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp);
if ($resp->isSuccess()) {
return true;
// verified!
} else {
$errors = $resp->getErrorCodes();
return false;
}
}
Like that question iOS: Authentication using XMLHttpRequest - Handling 401 reponse the easiest way to solve that is disregard natural headers validation and, on the callback sucess, validate with some flag.
I've saw some cases like that and never smell good.
Is your "remoteIP" variable correctly set in the client side?
Even if your Ajax request sends an empty or false value, the isset() function in your php script will return true, and thus populates the $remoteIp wrongly.
Try doing:
$remoteIp = $_SERVER['REMOTE_ADDR'];
Ajax just makes the browser do the request, thus PHP can perfectly grab the ip of our user.
I'm sure that if you're passing the wrong value, ReCaptcha will mess up in one way or another.
It's also safer to never trust any Javascript variables over Ajax, as those should be treated as user input as well.
The captcha is designed to prevent malicious clients (robots), so theoretically if a client bypasses the captcha, it is a server side problem. (However, if a client fails to complete the captcha, it may be a server side problem or a client side problem.)
So the problem must be at the server. Even from security considerations, you should be using $_SERVER['REMOTE_ADDR'] rather than $_POST['remoteIp'] because $_POST['remoteIp'] may be faked (by a malicious client). In fact, $_SERVER['REMOTE_ADDR'] is much more reliable than the client-side $_POST['remoteIp'].
I made a script 2 or 3 months ago that still works perfectly, try this:
<?php
$siteKey = ''; // Public Key
$secret = ''; // Private Key
/**
* This is a PHP library that handles calling reCAPTCHA.
* - Documentation and latest version
* https://developers.google.com/recaptcha/docs/php
* - Get a reCAPTCHA API Key
* https://www.google.com/recaptcha/admin/create
* - Discussion group
* http://groups.google.com/group/recaptcha
*
* #copyright Copyright (c) 2014, Google Inc.
* #link http://www.google.com/recaptcha
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A ReCaptchaResponse is returned from checkAnswer().
*/
class ReCaptchaResponse
{
public $success;
public $errorCodes;
}
class ReCaptcha
{
private static $_signupUrl = "https://www.google.com/recaptcha/admin";
private static $_siteVerifyUrl =
"https://www.google.com/recaptcha/api/siteverify?";
private $_secret;
private static $_version = "php_1.0";
/**
* Constructor.
*
* #param string $secret shared secret between site and ReCAPTCHA server.
*/
function ReCaptcha($secret)
{
if ($secret == null || $secret == "") {
die("To use reCAPTCHA you must get an API key from <a href='"
. self::$_signupUrl . "'>" . self::$_signupUrl . "</a>");
}
$this->_secret=$secret;
}
/**
* Encodes the given data into a query string format.
*
* #param array $data array of string elements to be encoded.
*
* #return string - encoded request.
*/
private function _encodeQS($data)
{
$req = "";
foreach ($data as $key => $value) {
$req .= $key . '=' . urlencode(stripslashes($value)) . '&';
}
// Cut the last '&'
$req=substr($req, 0, strlen($req)-1);
return $req;
}
/**
* Submits an HTTP GET to a reCAPTCHA server.
*
* #param string $path url path to recaptcha server.
* #param array $data array of parameters to be sent.
*
* #return array response
*/
private function _submitHTTPGet($path, $data)
{
$req = $this->_encodeQS($data);
$response = file_get_contents($path . $req);
return $response;
}
/**
* Calls the reCAPTCHA siteverify API to verify whether the user passes
* CAPTCHA test.
*
* #param string $remoteIp IP address of end user.
* #param string $response response string from recaptcha verification.
*
* #return ReCaptchaResponse
*/
public function verifyResponse($remoteIp, $response)
{
// Discard empty solution submissions
if ($response == null || strlen($response) == 0) {
$recaptchaResponse = new ReCaptchaResponse();
$recaptchaResponse->success = false;
$recaptchaResponse->errorCodes = 'missing-input';
return $recaptchaResponse;
}
$getResponse = $this->_submitHttpGet(
self::$_siteVerifyUrl,
array (
'secret' => $this->_secret,
'remoteip' => $remoteIp,
'v' => self::$_version,
'response' => $response
)
);
$answers = json_decode($getResponse, true);
$recaptchaResponse = new ReCaptchaResponse();
if (trim($answers ['success']) == true) {
$recaptchaResponse->success = true;
} else {
$recaptchaResponse->success = false;
$recaptchaResponse->errorCodes = $answers [error-codes];
}
return $recaptchaResponse;
}
}
$reCaptcha = new ReCaptcha($secret);
if(isset($_POST["g-recaptcha-response"])) {
$resp = $reCaptcha->verifyResponse(
$_SERVER["REMOTE_ADDR"],
$_POST["g-recaptcha-response"]
);
if ($resp != null && $resp->success) {echo "OK";}
else {echo "CAPTCHA incorrect";}
}
?>
<html>
<head>
<title>Google reCAPTCHA</title>
<script src="https://www.google.com/recaptcha/api.js"></script>
</head>
<body>
<form action="reCAPTCHA.php" method="POST">
<input type="submit" value="Submit">
<div class="g-recaptcha" data-sitekey="<?php echo $siteKey; ?>"></div>
</form>
</body>
</html>
Normally, it should work (just add your private key and your public key), I tested on my iPhone SE, 2 seconds ago, and it worked perfectly.
Related
I am trying to run a search on an API which requires my query data to be styled as XML nested in an XML request. I'll post my entire class and method calls (which I sent to iress tech support) so that it can be fully reviewed and in the off-chance that anyone has access to the same API, they can instantly reproduce the issue for themselves.
class XMLCurler
{
private $username = '[redacted]';
private $password = '[redacted]';
private $url = 'https://[redacted].xplan.iress.com.au/RPC2/';
public $ch; // the curl handle
public $token;
public $results;
public function __construct() {
if ($this->connect()) {
if ($this->login()) {
echo "<div class=\"success\">Successful Connection & Login. Token: {$this->token}</div>";
}
}
}
public function __destruct() {
if ($this->ch) {
$this->disconnect();
}
}
public function connect() {
if (!$this->ch = curl_init($this->url)) { // generate curl handle
echo "<div class=\"error\">CURL Error While Connecting (check url)";
return false;
}
return true;
}
public function disconnect() {
curl_close($this->ch);
}
public function processResponse($response) {
if (!$response) {
echo "<div class=\"error\">CURL Error While Attempting to Login - No XML token string<br><b>" , curl_error($this->ch) , "</b></div>";
return false;
}
$decoded = xmlrpc_decode($response);
if (is_array($decoded) && xmlrpc_is_fault($decoded)) {
echo "<div class=\"error\">Error Response: {$decoded['faultString']} ({$decoded['faultCode']})</div>";
return false;
}
return $decoded;
}
public function login() {
$postfields = xmlrpc_encode_request('edai.Login', array($this->username, $this->password)); // package as xml
curl_setopt($this->ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml'));
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_POSTFIELDS, $postfields);
curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, 0); // not advised, I need to find out how to avoid this
curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, 0); // not advised, I need to find out how to avoid this
if (!$token = $this->processResponse(curl_exec($this->ch))) {
return false;
}
if (!preg_match("~^[\w+]{20}$~", $token)) {
echo "<div class=\"error\">Invalid/Unexpected Token Generated<br><b>$token</b>";
return false;
}
$this->token = $token; // cache the valid token
return true;
}
public function listChildren($path) {
$method = "edai.ListChildren";
$request = xmlrpc_encode_request($method, array($this->token, $path));
echo "<div class=\"notice\">XMLRPC Encoded Request (for $method): <pre>" , htmlentities($request) , "</pre></div>";
curl_setopt($this->ch, CURLOPT_POSTFIELDS, $request);
if (!$results = $this->processResponse(curl_exec($this->ch))) {
return false;
}
$this->results = $results; // cache the valid results
return true;
}
public function search($basepath, $queryxml) {
$method = "edai.Search";
/** Desperate/Manual xml construction ...
* $xml = new DOMDocument("1.0", "utf-8");
* $xml->appendChild($methodCall = $xml->createElement("methodCall"));
* $methodCall->appendChild($methodName = $xml->createElement("methodName"));
* $methodCall->appendChild($params = $xml->createElement("params"));
* $params->appendChild($param1 = $xml->createElement("param"));
* $param1->appendChild($value1 = $xml->createElement("value"));
* $value1->appendChild($string1 = $xml->createElement("string"));
* $params->appendChild($param2 = $xml->createElement("param"));
* $param2->appendChild($value2 = $xml->createElement("value"));
* $value2->appendChild($string2 = $xml->createElement("string"));
* $params->appendChild($param3 = $xml->createElement("param"));
* $param3->appendChild($value3 = $xml->createElement("value"));
* $value3->appendChild($string3 = $xml->createElement("string"));
* $string3->appendChild($EntitySearch = $xml->createElement("EntitySearch"));
* $EntitySearch->appendChild($SearchResult1 = $xml->createElement("SearchResult"));
* $SearchResult1->setAttribute("field", "first_name");
* $EntitySearch->appendChild($SearchResult2 = $xml->createElement("SearchResult"));
* $SearchResult2->setAttribute('field', "last_name");
* $EntitySearch->appendChild($SearchQuick = $xml->createElement("SearchQuick"));
* $SearchQuick->appendChild($s = $xml->createElement("s"));
* $xpath = new DOMXPath($xml);
* $result1 = $xpath->query("//methodName");
* $result1->item(0)->nodeValue = $method;
* $result2 = $xpath->query("//params/param[1]/value/string");
* $result2->item(0)->nodeValue = $this->token;
* $result3 = $xpath->query("//params/param[2]/value/string");
* $result3->item(0)->nodeValue = "entitymgr/client";
* $result4 = $xpath->query("//SearchQuick/s");
* $result4->item(0)->nodeValue = "last_name:Smith";
* $xml->formatOutput = true;
* $request = $xml->saveXML();
*/
/** Desperately attempted passing array ...
* $queryarray = array(
* "EntitySearch" => array(
* array(
* "SearchResult" => array(
* "#attr" => array(
* "field" => "first_name"
* )
* )
* ),
* array(
* "SearchResult" => array(
* "#attr" => array(
* "field" => "last_name"
* )
* )
* ),
* array(
* "SearchQuick" => array(
* "s" => "last_name:Smith"
* )
* )
* )
* );
*/
$request = xmlrpc_encode_request($method, array($this->token, $basepath, $queryxml)); // this mutates the nested $queryxml string
// Causes:
//Error Response: UNKNOWN(CORBA.UNKNOWN(omniORB.UNKNOWN_PythonException, CORBA.COMPLETED_MAYBE)) (-32505)
//$request = html_entity_decode($request); // repair encoded entities
//$request = preg_replace('~(?:>\K\s+)|(?:\s+(?=<))~', '', $request); // strip every whitespace character between tags (hack)
// Causes:
// Error Response: ExpatError(syntax error: line 1, column 0 (byte 0)) (-32505)
echo "<div class=\"notice\">XMLRPC Encoded Request (for $method): <pre>" , htmlentities($request) , "</pre></div>";
curl_setopt($this->ch, CURLOPT_POSTFIELDS, $request);
if (!$results = $this->processResponse(curl_exec($this->ch))) {
return false;
}
$this->results = $results; // cache the valid results
return true;
}
}
Below is how I make the calls. edai.ListChildren works because I don't have to send any XML data. edai.Search does not work because I am failing to properly prepare the XML query within the XML request.
$XC = new XMLCurler();
/* edai.ListChildren works as desired/expected */
$path = "/entitymgr/client";
if ($XC->listChildren($path)) {
echo "<div>List of Children Successful.<pre>";
var_export($XC->results);
echo "</pre></div>";
}
/* edai.Search does not work */
$basepath = "entitymgr/client";
$queryxml = <<<XML
<EntitySearch>
<SearchResult field="first_name"/>
<SearchResult field="last_name"/>
<SearchQuick><s>last_name:Smith</s></SearchQuick>
</EntitySearch>
XML;
if ($XC->search($basepath, $queryxml)) {
echo "<div>Search Successful.<pre>";
var_export($XC->results);
echo "</pre></div>";
}
This is the attempted request and error message.
This is the relevant portion of the manual I was provided (XPLAN XML-RPC EXTERNAL DATA ACCESS INTERFACE 7 May 2013):
I have contacted iress.com a couple of weeks ago, they called me to loosely confirm that I was authorized to access the API, and told me that they'd be in touch -- that follow up call hasn't happened and I would like to get back to work on this project.
I do know for a fact that there is a last name of Smith to match my query.
I have no Python experience so the error responses are no help to me. I have made more hail mary attempts than I have posted, but I am tired of wasting my time. I don't know if the third parameter is meant to be nested inside of a <value>, <param>, <struct>, <string>, <array>, <xml>, or something else entirely.
If anyone has any suggestions regarding how to prepare my XML query for the request, I'll run them and supply feedback.
I am also happy to receive advice on the class design, security concerns, and completely different php approaches for getting the edai.Search to return some useful data.
As requested by #ThW, here is a collection of xml attempts and their respective error responses: https://pastebin.com/dYtwXWxz
A shot in the dark, since I can't test the API directly...
Perhaps the xmlrpc_encode_request call could do with named params:
$params = [
'context' => $this->token,
'basepath' => $basepath, // try with a leading slash, too, in spite of the docs listing it w/o one
'queryxml' => $queryxml,
];
$request = xmlrpc_encode_request($method, $params); // this mutates the nested $queryxml string
If that doesn't work, stop messing around with the code and install SoapUI or Postman or Insomnia or similar, and manually construct your requests.
I suspect you'll have a working request within a half-hour, and can work backwards from that to debug your code / rewrite your code. I'd do it for you if I had access to the API.
Things to check:
does the encoding make a difference (should it be utf8 instead of )?
the XML query needs to be treated as a string, so make sure it winds up encoded / wrapped in CDATA tags when your GUI client makes its requests. Depending on the client you pick it may be done for you, just make sure its done
After personally speaking to IRESS support and continued investigation, unfortunately (and despite a manual being written which expresses how to integrate with the API), the only licensed usage of the API is for "uploading documents with a Toshiba scanner".
To gain access, new license paperwork will need to be drawn up by the legal teams from IRESS and the company in the above redacted url. This is not likely to be a speedy endeavour.
It turns out that the error was not in my php code (which I have since developed further to better handle the response data), but rather the xml query that I was sending.
Instead of using what the documentation suggested:
<SearchQuick><s>last_name:Smith</s></SearchQuick>
Use the following equivalent valid expression:
<SearchByField field="last_name" op="equal"><s>Smith</s></SearchByField>
I use Redis server for sharing session between Php and Node js. For Node js client use "connect-redis" and for php client use redis-session-php and Predis. I took most of code from here gist upgraded version on stack (from correct answer).
app.js
var express = require('express'),
app = express(),
cookieParser = require('cookie-parser'),
session = require('express-session'),
RedisStore = require('connect-redis')(session);
app.use(express.static(__dirname + '/public'));
app.use(function(req, res, next) {
if (~req.url.indexOf('favicon'))
return res.send(404);
next();
});
app.use(cookieParser());
app.use(session({
store: new RedisStore({
// this is the default prefix used by redis-session-php
prefix: 'session:php:'
}),
// use the default PHP session cookie name
name: 'PHPSESSID',
secret: 'node.js rules',
resave: false,
saveUninitialized: false
}));
app.use(function(req, res, next) {
req.session.nodejs = 'Hello from node.js!';
res.send('<pre>' + JSON.stringify(req.session, null, ' ') + '</pre>');
});
app.listen(8080);
app.php
<?php
// this must match the express-session `secret` in your Express app
define('EXPRESS_SECRET', 'node.js rules');
// ==== BEGIN express-session COMPATIBILITY ====
// this id mutator function helps ensure we look up
// the session using the right id
define('REDIS_SESSION_ID_MUTATOR', 'express_mutator');
function express_mutator($id) {
if (substr($id, 0, 2) === "s:")
$id = substr($id, 2);
$dot_pos = strpos($id, ".");
if ($dot_pos !== false) {
$hmac_in = substr($id, $dot_pos + 1);
$id = substr($id, 0, $dot_pos);
}
return $id;
}
// check for existing express-session cookie ...
$sess_name = session_name();
if (isset($_COOKIE[$sess_name])) {
// here we have to manipulate the cookie data in order for
// the lookup in redis to work correctly
// since express-session forces signed cookies now, we have
// to deal with that here ...
if (substr($_COOKIE[$sess_name], 0, 2) === "s:")
$_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 2);
$dot_pos = strpos($_COOKIE[$sess_name], ".");
if ($dot_pos !== false) {
$hmac_in = substr($_COOKIE[$sess_name], $dot_pos + 1);
$_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 0, $dot_pos);
// https://github.com/tj/node-cookie-signature/blob/0aa4ec2fffa29753efe7661ef9fe7f8e5f0f4843/index.js#L20-L23
$hmac_calc = str_replace("=", "", base64_encode(hash_hmac('sha256', $_COOKIE[$sess_name], EXPRESS_SECRET, true)));
if ($hmac_calc !== $hmac_in) {
// the cookie data has been tampered with, you can decide
// how you want to handle this. for this example we will
// just ignore the cookie and generate a new session ...
unset($_COOKIE[$sess_name]);
}
}
} else {
// let PHP generate us a new id
session_regenerate_id();
$sess_id = session_id();
$hmac = str_replace("=", "", base64_encode(hash_hmac('sha256', $sess_id, EXPRESS_SECRET, true)));
// format it according to the express-session signed cookie format
session_id("s:$sess_id.$hmac");
}
// ==== END express-session COMPATIBILITY ====
require('redis-session-php/redis-session.php');
RedisSession::start();
$_SESSION["php"] = "Hello from PHP";
if (!isset($_SESSION["cookie"]))
$_SESSION["cookie"] = array();
echo "<pre>";
echo json_encode($_COOKIE, JSON_PRETTY_PRINT);
echo json_encode($_SESSION, JSON_PRETTY_PRINT);
echo "</pre>";
?>
Problem is that: When first execute "php file" then execute "node js server page" - "node js server page" have not seen session creation from "php file". When vice versa (first execute "node js server page" then execute "php file") session variables have seen in both page
result app.php
[]{
"php": "Hello from PHP",
"cookie": []
}
result node js page (http://127.0.0.1:8080)
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"nodejs": "Hello from node.js!"
}
You are probably facing a cross domain issue. If you are running PHP and Node in a different address or port than PHP (and probably you are), HTML won't share Cookies between requests that go to the other domain, it will keep separated copies to each domain.
If you are using subdomains (for example, your PHP in a URL like app1.mydomain.com and your NodeJS running in app2.mydomain.com), you can share your cookies configuring them to be set/read using the main domain cookie path ( mydomain.com ).
There is good information about this topic over here:
Using Express and Node, how to maintain a Session across subdomains/hostheaders.
Let me us if you need more information or if your problem is not exactly that one.
i solve this problem from this article: PHP and Node.JS session share using Redis
app.js
var app = require("http").createServer(handler),
fs = require("fs"),
redis = require("redis"),
co = require("./cookie.js");
app.listen(443);
//On client incomming, we send back index.html
function handler(req, res) {
//Using php session to retrieve important data from user
var cookieManager = new co.cookie(req.headers.cookie);
//Note : to specify host and port : new redis.createClient(HOST, PORT, options)
//For default version, you don't need to specify host and port, it will use default one
var clientSession = new redis.createClient();
console.log('cookieManager.get("PHPSESSID") = ' + cookieManager.get("PHPSESSID"));
clientSession.get("sessions/" + cookieManager.get("PHPSESSID"), function(error, result) {
console.log("error : " + result);
if(error) {
console.log("error : " + error);
}
if(result != null) {
console.log("result exist");
console.log(result.toString());
} else {
console.log("session does not exist");
}
});
//clientSession.set("sessions/" + cookieManager.get("PHPSESSID"), '{"selfId":"salamlar22", "nodejs":"salamlar33"}');
}
cookie.js
//Directly send cookie to system, if it's node.js handler, send :
//request.headers.cookie
//If it's socket.io cookie, send :
//client.request.headers.cookie
module.exports.cookie = function(co){
this.cookies = {};
co && co.split(';').forEach(function(cookie){
var parts = cookie.split('=');
this.cookies[parts[0].trim()] = (parts[1] || '').trim();
}.bind(this));
//Retrieve all cookies available
this.list = function(){
return this.cookies;
};
//Retrieve a key/value pair
this.get = function(key){
if(this.cookies[key]){
return this.cookies[key];
}else{
return {};
}
};
//Retrieve a list of key/value pair
this.getList = function(map){
var cookieRet = {};
for(var i=0; i<map.length; i++){
if(this.cookies[map[i]]){
cookieRet[map[i]] = this.cookies[map[i]];
}
}
return cookieRet;
};
};
app.php
<?php
include 'redis.php';
session_start();
echo '<pre>';
var_dump($_COOKIE);
echo '</pre>';
echo '$_SESSION["nodejs"] = '.$_SESSION[selfId].'<br>';
$_SESSION[selfId] = 2;
echo '$_SESSION["nodejs"] = '.$_SESSION[selfId].'<br>';
?>
redis.php
<?php
//First we load the Predis autoloader
//echo dirname(__FILE__)."/predis-1.0/src/Autoloader.php";
require(dirname(__FILE__)."/redis-session-php/modules/predis/src/Autoloader.php");
//Registering Predis system
Predis\Autoloader::register();
/**
* redisSessionHandler class
* #class redisSessionHandler
* #file redisSessionHandler.class.php
* #brief This class is used to store session data with redis, it store in json the session to be used more easily in Node.JS
* #version 0.1
* #date 2012-04-11
* #author deisss
* #licence LGPLv3
*
* This class is used to store session data with redis, it store in json the session to be used more easily in Node.JS
*/
class redisSessionHandler{
private $host = "127.0.0.1";
private $port = 6379;
private $lifetime = 0;
private $redis = null;
/**
* Constructor
*/
public function __construct(){
$this->redis = new Predis\Client(array(
"scheme" => "tcp",
"host" => $this->host,
"port" => $this->port
));
session_set_save_handler(
array(&$this, "open"),
array(&$this, "close"),
array(&$this, "read"),
array(&$this, "write"),
array(&$this, "destroy"),
array(&$this, "gc")
);
}
/**
* Destructor
*/
public function __destruct(){
session_write_close();
$this->redis->disconnect();
}
/**
* Open the session handler, set the lifetime ot session.gc_maxlifetime
* #return boolean True if everything succeed
*/
public function open(){
$this->lifetime = ini_get('session.gc_maxlifetime');
return true;
}
/**
* Read the id
* #param string $id The SESSID to search for
* #return string The session saved previously
*/
public function read($id){
$tmp = $_SESSION;
$_SESSION = json_decode($this->redis->get("sessions/{$id}"), true);
if(isset($_SESSION) && !empty($_SESSION) && $_SESSION != null){
$new_data = session_encode();
$_SESSION = $tmp;
return $new_data;
}else{
return "";
}
}
/**
* Write the session data, convert to json before storing
* #param string $id The SESSID to save
* #param string $data The data to store, already serialized by PHP
* #return boolean True if redis was able to write the session data
*/
public function write($id, $data){
$tmp = $_SESSION;
session_decode($data);
$new_data = $_SESSION;
$_SESSION = $tmp;
$this->redis->set("sessions/{$id}", json_encode($new_data));
$this->redis->expire("sessions/{$id}", $this->lifetime);
return true;
}
/**
* Delete object in session
* #param string $id The SESSID to delete
* #return boolean True if redis was able delete session data
*/
public function destroy($id){
return $this->redis->delete("sessions/{$id}");
}
/**
* Close gc
* #return boolean Always true
*/
public function gc(){
return true;
}
/**
* Close session
* #return boolean Always true
*/
public function close(){
return true;
}
}
new redisSessionHandler();
?>
I'm developing a RESTful web service and, honestly, it is my first ws. I decided to use PHP because I think that I know that language.
This is my RestHandler object, but when I debug the request, accessing to a method not implemented, with Charles, it returns the right response, but 500 error instead of 400. Why?
class RestHandler {
private $method;
private $actionName;
/**
* #param $method
* #param $action
*/
public function __construct($method, $action)
{
$this->method = $method;
$this->actionName = $action;
if (isset($this->method) && isset($this->actionName))
{
if (! method_exists($this, $this->actionName))
{
// Action is not implemented in the object.
$this->handleErrorReturning("Not implemented method.", 400);
return;
}
// OK, proceed with actions
$this->handleProceedRequest();
}
else
{
// Return error 406 Missing parameter
$this->handleErrorReturning("Missing parameter", 406);
}
}
private function handleProceedRequest()
{
if (strcasecmp($this->method, "get") == 0)
{
// No JSON to read
}
}
/**
* #param $errorDescription
* #param $errorCode
*/
private function handleErrorReturning($errorDescription, $errorCode)
{
header($_SERVER["SERVER_PROTOCOL"]." ".$errorDescription." ".$errorCode);
header('Content-Type: application/json; charset=utf-8');
$errorResponse = new ResponseError($errorCode, $errorDescription);
echo $errorResponse;
}
}
This is the Charles snapshot
SOLVED
I inverted errorDescription with errorCode and now it works. It was a stupid error. Thanks
Try setting your header like this:
header($_SERVER['SERVER_PROTOCOL'] . ' Not implemented method', true, 400);
I am trying to use paypal lib https://github.com/jersonandyworks/Paypal-Library-by-RomyBlack on my Codeigniter project.
I am able to navigate to paypal n pay for the product on sandbox, the problem is
There is no cancel_retun option on paypal.
There is no return url after the payment is completed
The below code is my controller code.
$this->load->library('paypal');
$config['business'] = 'QD8HYTTSE4M38';
$config['cpp_header_image'] = ''; //Image header url [750 pixels wide by 90 pixels high]
$config['return'] = 'main/viewAds/info.php';
//echo $config['return'];
$config['cancel_return'] = $this->config->base_url() .'main/viewAds/22';
$config['notify_url'] = $this->config->base_url() .'main/viewAds/30';
$config['production'] = FALSE; //Its false by default and will use sandbox
$config["invoice"] = '843843'; //The invoice id
$this->load->library('paypal',$config);
#$this->paypal->add(<name>,<price>,<quantity>[Default 1],<code>[Optional]);
$this->paypal->add('T-shirt',1,1); //First item
$this->paypal->pay(); //Proccess the payment
The below is the library
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* CodeIgniter
*
* An open source application development framework for PHP 5.1.6 or newer
*
* #package CodeIgniter
* #author Romaldy Minaya
* #copyright Copyright (c) 2011, PROTOS.
* #license GLP
* #since Version 1.0
* #version 1.0
*/
// ------------------------------------------------------------------------
/**
* Paypal Class
*
* #package CodeIgniter
* #subpackage Libraries
* #category Payment process
* #author Romaldy Minaya
*
// ------------------------------------------------------------------------
Documentation
This class let you make the payment procces based on paypal API,
effortless and easy.
*1)Use the same documentation about the vars from paypal page.
*2)Customize the payment procces as you desire.
*3)Build with love.
Implementation
*1)Copy this code in your controller's function
$config['business'] = 'demo#demo.com';
$config['cpp_header_image'] = ''; //Image header url [750 pixels wide by 90 pixels high]
$config['return'] = 'sucess.php';
$config['cancel_return'] = 'shopping.php';
$config['notify_url'] = 'process_payment.php'; //IPN Post
$config['production'] = TRUE; //Its false by default and will use sandbox
$config['discount_rate_cart'] = 20; //This means 20% discount
$config["invoice"] = '843843'; //The invoice id
$this->load->library('paypal',$config);
#$this->paypal->add(<name>,<price>,<quantity>[Default 1],<code>[Optional]);
$this->paypal->add('T-shirt',2.99,6); //First item
$this->paypal->add('Pants',40); //Second item
$this->paypal->add('Blowse',10,10,'B-199-26'); //Third item with code
$this->paypal->pay(); //Proccess the payment
The notify url is where paypal will POST the information of the payment so you
can save that POST directly into your DB and analize as you want.
With $config["invoice"] is how you identify a bill and you can compare,save or update
that value later on your DB.
For test porpuses i do recommend to save the entire POST into your DB and analize if
its working according to your needs before putting it in production mode. EX.
$received_post = print_r($this->input->post(),TRUE);
//Save that variable and analize.
Note: html reference page http://bit.ly/j4wRR
*/
class Paypal {
var $config = Array();
var $production_url = 'https://www.paypal.com/cgi-bin/webscr?';
var $sandbox_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?';
var $item = 1;
/**
* Constructor
*
* #param string
* #return void
*/
public function __construct($props = array())
{
$this->__initialize($props);
log_message('debug', "Paypal Class Initialized");
}
// --------------------------------------------------------------------
/**
* initialize Paypal preferences
*
* #access public
* #param array
* #return bool
*/
function __initialize($props = array())
{
#Account information
$config["business"] = 'QD8HYTTSE4M38'; //Account email or id
$config["cmd"] = '_cart'; //Do not modify
$config["production"] = FALSE;
#Custom variable here we send the billing code-->
$config["custom"] = '';
$config["invoice"] = ''; //Code to identify the bill
#API Configuration-->
$config["upload"] = '1'; //Do not modify
$config["currency_code"] = 'USD'; //http://bit.ly/anciiH
$config["disp_tot"] = 'Y';
#Page Layout -->
$config["cpp_header_image"] = ''; //Image header url [750 pixels wide by 90 pixels high]
$config["cpp_cart_border_color"] = '000'; //The HTML hex code for your principal identifying color
$config["no_note"] = 1; //[0,1] 0 show, 1 hide
#Payment Page Information -->
$config["return"] = ''; //The URL to which PayPal redirects buyers’ browser after they complete their payments.
$config["cancel_return"] = ''; //Specify a URL on your website that displays a “Payment Canceled†page.
$config["notify_url"] = ''; //The URL to which PayPal posts information about the payment (IPN)
$config["rm"] = '2'; //Leave this to get payment information
$config["lc"] = 'EN'; //Languaje [EN,ES]
#Shipping and Misc Information -->
$config["shipping"] = '';
$config["shipping2"] = '';
$config["handling"] = '';
$config["tax"] = '';
$config["discount_amount_cart"] = ''; //Discount amount [9.99]
$config["discount_rate_cart"] = ''; //Discount percentage [15]
#Customer Information -->
$config["first_name"] = '';
$config["last_name"] = '';
$config["address1"] = '';
$config["address2"] = '';
$config["city"] = '';
$config["state"] = '';
$config["zip"] = '';
$config["email"] = '';
$config["night_phone_a"] = '';
$config["night_phone_b"] = '';
$config["night_phone_c"] = '';
/*
* Convert array elements into class variables
*/
if (count($props) > 0)
{
foreach ($props as $key => $val)
{
$config[$key] = $val;
}
}
$this->config = $config;
}
// --------------------------------------------------------------------
/**
* Perform payment process
*
* #access public
* #param array
* #return void
*/
function pay(){
#Convert the array to url encode variables
$vars = http_build_query($this->config);
if($this->config['production'] == TRUE){
header('LOCATION:'.$this->production_url.$vars);
}else{
header('LOCATION:'.$this->sandbox_url.$vars);
}
}
// --------------------------------------------------------------------
/**
* Add a product to the list
*
* #access public
* #param array
* #return void
*/
function add($item_name = '',$item_amount = NULL,$item_qty = NULL,$item_number = NULL){
$this->config['item_name_'.$this->item] = $item_name;
$this->config['amount_'.$this->item] = $item_amount;
$this->config['quantity_'.$this->item] = $item_qty;
$this->config['item_number_'.$this->item] = $item_number;
$this->item++;
}
}
// END Paypal Class
/* End of file Paypal.php /
/ Location: ./application/libraries/Paypal.php */
This is the transaction page i get
I am expecting it to return to my web site but it just stays there.
Kindly advice me on what to do. Thanks.
This library you're using is apparently using PayPal Standard, which does not guarantee the user will be returned to your site.
You can enable Auto-Return from within your PayPal account profile, but still, if the user closes the browser or goes somewhere else on their prior to the redirect happening they won't make it there.
If you want to ensure you always get back to PayPal you'll need to switch to the Express Checkout API, which does guarantee that the user will end up back at your site. I have a PHP Class Library for PayPal that will make this very simple for you. That is my primary version that I continue to maintain, and it works with Composer so you can use that to autoload and make it available in CI.
Alternatively, I do have an old CI specific version of the library you might want to use instead. It's not far behind right now, but I'm not going to maintain it like I am the primary one.
Either way, you'll be working with SetExpressCheckout, GetExpressCheckoutDetails, and DoExpressCheckoutPayment.
I would also recommend you take a look at Instant Payment Notification (IPN). It will allow you to automate tasks in real-time based on transactions that hit your account regardless of whether or not the user makes it back to your site.
I am currently developing a modification for an open source forum software. This modification allows an user to donate through that forum software.
However, recently an user reported an issue which may be caused by my code. I use another open source library to handle the IPN connection - An IPN Listener PHP class.
The user who reported this issue is receiving the following e-mail:
Hello <My Name>,
Please check your server that handles PayPal Instant Payment
Notifications (IPN). Instant Payment Notifications sent to the
following URL(s) are failing:
http://www.MySite.com/donate/handler.php
If you do not recognize this URL, you may be using a service provider
that is using IPN on your behalf. Please contact your service provider
with the above information. If this problem continues, IPNs may be
disabled for your account.
Thank you for your prompt attention to this issue.
Sincerely, PayPal
I am fearing that the issue comes from my side, therefore I have to look into this and make sure.
I lightly modified the IPN Listener script, which leads me to think that my modification is causing this issue. Paypal also had some changes recently which might have provoked this problem.
This is how the class looks like momentarily:
/**
* 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) 2011 - Micah Carrick
* #version 2.0.5
* #license http://opensource.org/licenses/gpl-license.php
*
* This library is originally licensed under GPL v3, but I received
* permission from the author to use it under GPL v2.
*/
class ipn_handler
{
/**
* 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 www.sandbox.paypal.com is used for the
* post back. If false, the live URI 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 = 60;
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)
{
global $user;
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_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($user->lang['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)
{
global $user;
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($user->lang['FSOCKOPEN_ERROR'] . "[$errno] $errstr");
}
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Content-Length: " . strlen($encoded_data) . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Host: " . $this->getPaypalHost() . "\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 ipn_handler::SANDBOX_HOST;
}
else
{
return ipn_handler::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)
{
global $user;
$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($user->lang['NO_POST_DATA']);
}
}
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($user->lang['INVALID_RESPONSE'] . $this->response_status);
}
if (strpos(trim($this->response), "VERIFIED") !== false)
{
return true;
}
elseif (trim(strpos($this->response), "INVALID") !== false)
{
return false;
}
else
{
throw new Exception($user->lang['UNEXPECTED_ERROR']);
}
}
/**
* Require Post Method
*
* Throws an exception and sets a HTTP 405 response header if the request
* method was not POST.
*/
public function requirePostMethod()
{
global $user;
// require POST requests
if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST')
{
header('Allow: POST', true, 405);
throw new Exception($user->lang['INVALID_REQUEST_METHOD']);
}
}
}
Is there any issue with this script which is causing this problem?
P.S: The URL donate/handler.php is indeed the IPN handler/listener file, so it's a recognized URL.
For the debug part
You can also check on Paypal the state of your IPN.
My Account > History > IPN History.
It will list all IPN that were sent to your server. You will see a status for each of them. It might help. But as Andrew Angell says, take a look at your log.
For the PHP part
Paypal provide a lots a goodness stuff on their Github. You should definitively take a closer look.
They have a dead simple IPNLister sample that you should use (instead of a custom one - even if it seems good). It use built-in function from Paypal itself. And I personally use it too. You shouldn't re-invent the wheel :)
<?php
require_once('../PPBootStrap.php');
// first param takes ipn data to be validated. if null, raw POST data is read from input stream
$ipnMessage = new PPIPNMessage(null, Configuration::getConfig());
foreach($ipnMessage->getRawData() as $key => $value) {
error_log("IPN: $key => $value");
}
if($ipnMessage->validate()) {
error_log("Success: Got valid IPN data");
} else {
error_log("Error: Got invalid IPN data");
}
As you can see, it's simple.
I use it in a slightly different way:
$rawData = file_get_contents('php://input');
$ipnMessage = new PPIPNMessage($rawData);
$this->forward404If(!$ipnMessage->validate(), 'IPN not valid.');
$ipnListener = new IPNListener($rawData);
$ipnListener->process();
The IPNListener class is custom to me: it does handle what to do with the IPN. It parse the response and do action depending on the state:
function __construct($rawData)
{
$rawPostArray = explode('&', $rawData);
foreach ($rawPostArray as $keyValue)
{
$keyValue = explode ('=', $keyValue);
if (count($keyValue) == 2)
{
$this->ipnData[$keyValue[0]] = urldecode($keyValue[1]);
}
}
// log a new IPN and save in case of error in the next process
$this->ipn = new LogIpn();
$this->ipn->setContent($rawData);
$this->ipn->setType(isset($this->ipnData['txn_type']) ? $this->ipnData['txn_type'] : 'Not defined');
$this->ipn->save();
}
/**
* Process a new valid IPN
*
*/
public function process()
{
if (null === $this->ipnData)
{
throw new Exception('ipnData is empty !');
}
if (!isset($this->ipnData['txn_type']))
{
$this->ipn->setSeemsWrong('No txn_type.');
$this->ipn->save();
return;
}
switch ($this->ipnData['txn_type'])
{
// handle statues
}
}
Check your web server logs. That will show you what result is coming up when the IPN script is hit, and since it's failing you must be getting some sort of 500 internal server error. The logs will give you the error info that you would normally see on screen, like a syntax error, line number, etc.
What I like to do for troubleshooting, too, is create a simulator of my own by building a basic HTML form with the action set to the URL of my IPN listener. Add hidden fields with the names/values you'd expect to get from an IPN and then you can load that in a browser and submit it directly so that you can see the result on screen. You'll probably find that you have an error in your code somewhere causing the script to be unable to complete.
Keep in mind that when testing this way the data isn't coming from PayPal so it will not be verified. You'll need to make sure your code logic is setup to handle that accordingly.
Once you're able to get everything running smoothly testing that way, I'd use the PayPal IPN listener as another confirmation, and then you can rest assured that you've fixed the issue.
I highly recommend using the IPN simulator in the Paypal developer site. It can build an IPN request for you and send it to your server and report back what it got.
Check your logs under your Paypal account, it will show a list of IPN requests sent and also the results. If you have some major issues you can use the GET string provided to test out use cases.
There was indeed an issue with my code. I did some casting mistake and it caused the IPN to fail. This issue has been resolved.
Thanks for the help everyone.