TYPO3: How can I allow third party query string parameters? - forms

I have created a multistep order form extension which collects some user data and then redirect to different third party solutions for the payment step.
This third party solutions (like PayPal) require a return url to find the way back to my form. So the current workflow looks like this:
My form: collect user data
My form: redirect to payment provider with return url
Payment provider: collect user data for payment
Payment provider: redirect back to my form
My form: show order details with collected data
Payment provider: submit order
My form: redirect to success page
I hope this is comprehensible so far. I stuck into step 4, because every redirect from a payment provider back to my form ends in a 404, because the payment provider add some query string parameters which my form don't know. I think the problem here is that this parameters are not cHash calculated and this is the reason because they fail.
In step 2 I generate the return url of my form, this looks like this:
$returnUrl = $this->uriBuilder
->reset()
->setTargetPageUid($returnPageId)
->setCreateAbsoluteUri(true)
->setSection('form-multistep')
->uriFor('step5', null, 'CouponItem', 'bookingmanager', 'p2');
The generated output looks like this:
https://example.com/coupon/?tx_bookingmanager_p2%5Baction%5D=step5&tx_bookingmanager_p2%5Bcontroller%5D=CouponItem&cHash=565dc2e51a8d43bf3836b43b994e98d0#form-multistep
So this is the url which will be send to different payment providers and if I copy paste this url into my browser this url is working, BUT the payment providers manipulate this url and add some custom query string parameters to it before they redirect.
For example PayPal add the following query string parameters:
paymentId, token and PayerID
So the generated redirect url from PayPal to my form looks like:
https://example.com/coupon/?paymentId=XXXXXX&token=XXXXXX&PayerID=XXXXXX&tx_bookingmanager_p2%5Baction%5D=step5&tx_bookingmanager_p2%5Bcontroller%5D=CouponItem&cHash=565dc2e51a8d43bf3836b43b994e98d0#form-multistep
And this url ends in a 404 because I think the cHash is not valid for this manipulated url anymore or am I wrong?
Furthermore I have tried to predefine this parameters, like so:
$returnUrl = $this->uriBuilder
->reset()
->setTargetPageUid($this->settings['returnPageId'])
->setCreateAbsoluteUri(true)
->setSection('form-multistep')
->setArguments(['paymentId' => '', 'token' => '', 'PayerID' => ''])
->uriFor('step5',null,'CouponItem','bookingmanager','p2');
If I do something like that then the redirect from PayPal to my form is working, BUT unfortunately not as expected, because PayPal don't know that the parameters already exist in the given return url and still add this parameters again, so the generated url looks like:
https://example.com/coupon/?paymentId=&token=&PayerID=&tx_bookingmanager_p2%5Baction%5D=step5&tx_bookingmanager_p2%5Bcontroller%5D=CouponItem&cHash=bf642fb35a66033689b7d4ff772b3cf9#form-multistep&paymentId=XXXX&token=XXXX&PayerID=XXXX
Furthermore I can't access the query string parameters which PayPal added to the url :(. So I have tried something like this:
$returnUrl = $this->uriBuilder
->reset()
->setTargetPageUid($this->settings['returnPageId'])
->setCreateAbsoluteUri(true)
->setSection('form-multistep')
->uriFor('step5',null,'CouponItem','bookingmanager','p2');
$this->uriBuilder
->reset()
->setTargetPageUid($this->settings['returnPageId'])
->setCreateAbsoluteUri(true)
->setSection('form-multistep')
->setArguments(['paymentId' => '', 'token' => '', 'PayerID' => ''])
->uriFor('step5',null,'CouponItem','bookingmanager','p2');
So I have sent the "normal" return url (with only my extension parameters) to PayPal and register a second url with the uri builder to get the redirect from PayPal to my form working (this already seems to be bad).
So the redirect to PayPal through my form and the redirect from PayPal back to my form is working, BUT now I got the same problem as my last try. I got the right parameters in my URL, but I can't access them:
DebuggerUtility::var_dump(GeneralUtility::_GET());
array(3 items)
tx_bookingmanager_p2 => array(2 items)
action => 'step5' (5 chars)
controller => 'CouponItem' (10 chars)
cHash => 'cbe7c08c1a45e85404a06877c453cb63' (32 chars)
id => '175' (3 chars)
So how can I allow custom query string parameters which are generated by a third party app for a specific controller action?

You need to exclude those parameters from cHash.
In the InstallTool you will find a value for
$GLOBALS['TYPO3_CONF_VARS']['FE']['cHashExcludedParameters']
where you enter the list of paramters (comma separated) you do not want to be consiered in the cHash.

Related

php8 and Paypal IPN setup: Where does db INSERT upon successful handshake go?

Of the three files here- https://github.com/paypal/ipn-code-samples/tree/master/php
I have my Webhook URL set to the stock github version of- PaypalIPN.php (this validates successfully 100% of the time, if I use example_usage.php... Doesn't work. If I use both as Webhooks... Doesn't work).
From the Paypal button side of things I'm able to post my website's active user (call him $MrUser) with this:
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
"custom_id":"<?php echo $MrUser; ?>",
"description":"One hundred Webdollars",
"amount":
{
"currency_code":"USD",
"value":1.99
}
}]
});
},
Here's the SQL I need to run upon successful validation (I change $MrUser to $kitty for clarity's sake):
require 'sqlconfig.php';
$dsn = "mysql:host=$host;dbname=$db;charset=UTF8";
try {
$pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
echo $e->getMessage();
}
$hashedIP = $_SERVER['REMOTE_ADDR'];
$kitty = $_POST['custom']; // Not sure this is working yet, but this should give me $mrUser;
$sql = "INSERT INTO `Insert_upon_Paypal_success` (`date`,`hashedIP`,`username`,`webdollarAMT`) VALUES (now(),:hashedIP,:kitty,'100')";
$statement = $pdo->prepare($sql);
$statement->bindValue(':hashedIP', $hashedIP);
$statement->bindValue(':kitty', $kitty);
$inserted = $statement->execute();
I'm popping this into the PaypalIPN.php file upon validation, but, it doesn't work. Here's how I have it in there:
// Check if PayPal verifies the IPN data, and if so, return true.
if ($res == self::VALID) {
return true;
// i.e. putting all of the SQL above right here.
} else {
return false;
}
I'm guessing I need to put the SQL in a specific place that I'm missing, as per the layout of the PaypalIPN.php file... Please help!!
There is no reason to use IPN with current PayPal Checkout integrations. It is very old technology (20+ years) and should be deprecated soon.
Webhooks are a successor to IPN. However, even they are unnecessary for normal payment processing -- better used only if you need automated notifications of post-checkout exceptions such as refunds or disputes.
For normal PayPal payments, do not use either.
Instead, use the v2/checkout/orders API and make two routes (url paths) on your server, one for 'Create Order' and one for 'Capture Order'. You could use the (recently deprecated) Checkout-PHP-SDK for the routes' API calls to PayPal, or your own HTTPS implementation of first getting an access token and then doing the call with PHP's curl or similar. Both of these routes should return/output only JSON data (no HTML or text). Inside the 2nd route, when the capture API is successful you should verify the amount was correct and store its resulting payment details in your database (particularly purchase_units[0].payments.captures[0].id, which is the PayPal transaction ID) and perform any necessary business logic (such as reserving product or sending an email) immediately before forwarding return JSON to the frontend caller. In the event of an error forward the JSON details of it as well, since the frontend must handle such cases.
Pair those 2 routes with this frontend approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server . (If you need to send any additional data from the client to the server, such as an items array or selected options, add a body parameter to the fetch with a value that is a JSON string or object)

ZF2 how to populate a form that failed a POST request into a GET redirect

I'm following REST standard where you use a POST action to create a resource and GET to show data.
That includes using GET to show a creation form and POST to handle the actual creation of the resource (AKA, saving to database).
In the case the POST request fails (lets say, a duplicate email address), a 302 is returned as a response, redirecting the user back to the form (kind of as a GET /resource/create with 302).
How do I persist the data sent from POST after the redirection in ZF2?
Or, maybe ZF2 doesn't support this/we're not supposed to do a 302 redirection?
I just think you want to pass data back to form. And display data on each fields. Usually, for failed request you don't need make redirection. Just display the form and data. Just use redirect when the process success (saving to database).
$form = new Form(); // your form
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
// saving data here then redirect
$this->redirect()->toRoute('route', array('action' => 'name'), array('param => 1'));
}
}
return array('form' => $form);

Can't get my form to be valid

I want to make a ReST infrastructure with a Symfony server (using FOSRestBundle).
there is
a Contact entity, (id, name, email)
a AddType form to add a new contact
On the client side, I have a form which sends a POST request whose body is
{"the_name_of_my contact_form":{"name":"foo", "email": "foo#example.org"}}
My controller (which extends FOSRestController) can see the data in the request
$request->request->get($form->getName()) returns {"name":"foo", "email": "foo#example.org"}
But whether I use $form->handleRequest($request) or $form->submit($data)
$form->isValid() is always false
I hope this is clear enough... can anyone help?
This problem is related to the CRSF validation. You have to disable it for the user requesting the service. You can disable it in you config.yml. You'll have to set something like this:
fos_rest:
disable_csrf_role: ROLE_API
Just make sure that the user requesting your service has this role. You can read more about user roles here.
Also, you'll have to submit you form, not handle it. Here is an snippet of it:
$form = $this->createForm(ProjectType::class, $project);
$form->submit($request->request->all());
if ($form->isValid()) {

playframework 2.x form redirect with prefill

I'm trying to implement a user login form, I want to achieve:
when username doesn't exist in DB, return a flash message with previous
form data prefilled.
if any server side validation errors happened, return back to previous page, with old data pre-filled, display the errors
messages along side.
The problem now is, if I use flash scope, I need to use Redirect after post, but this will lose the pre-filled data.
If I use any status other than Redirect, I can't put data into flash scope.
What did I missing?
Don't use a Redirect for a failed login. You can return the same Form back to the login view with extra errors attached to it.
Something like this:
loginForm.bindFromRequest.fold(
formWithErrors => views.html.login(formWithErrors),
credentials => {
if(authenticate(credentials)) // dummy implementation
Redirect(controllers.Application.index)
else
BadRequest(views.html.login(loginForm.fill(credentials).withGlobalError("Incorrect login credentials")))
}
)
Then your view signature would look something like this:
#(loginForm: Form[Credentials])
#* Displays the first global error from the form, if any. *#
#loginForm.globalError.map{error =>
<h3>#error.message</h3>
}
And you'd pre-fill the form with values as before (I hope).
If there are multiple global errors, you can access them with globalErrors as it will access a Seq[FormError] instead of Option[FormError].
You can also attach errors to specific Form keys.
loginForm.withError("email", "I don't like your email.")
And would access them similarly:
#loginForm.error("email").map{ error =>
#error.message
}
You are using wrong concept, take a look to the Handling form submission doc, section: Validating a form in an Action
If form contains errors you are returning BadRequest (not Redirect)
If form IS valid anyway next check (i.e. DB query) returns an error you should do exactly the same thing as in formWithErrors so render the view passing incoming form to it (userData)
Finally if everything's OK, you can make your operation and redirect the user i.e. to main page or something...
Pseudo code (basing on doc):
def userPost = Action { implicit request =>
userForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.user(formWithErrors))
},
userData => {
// Check whatever you need...
if (afterCheckSomethingIsWrong){
// if something's wrong fill the `userForm` with `userData` and render the same view again...
// You can use flash scope here i.e. for placing error message
BadRequest(views.html.user(userForm.fill(userData))).flashing("error" -> "The account doesn't exist")
} else {
// if everything is OK, redirect to some page, outside the form handling process, i.e. main page
Redirect(routes.Application.index).flashing("success" -> "Fine you're logged now")
}
}
)
}

Redirect to referer after a POST Request

I have a web application in Play. The web application consists of several pages. In every page there is a small flag that enables the user to change the language (locale) from german to english and back.
I handle this with a redirect to referer:
def referer(implicit request: Request[AnyContent]) =
request.headers.get(REFERER).getOrElse(mainUrl)
def locale(l: String) = Authenticated { user =>
implicit request =>
Redirect(referer).withCookies(Cookie(LANG, if (l == "de" || l == "en") l else "de"))
}
It is working fine. Well, at least for GET requests.
I have a specific page where the user has to input data in a form. This form is then POSTed to the server. Were errors found, the form is displayed again with the error messages, as usual. Now, if the user wants to change the language (by clicking on the flag), the redirect to referer does not work, because it tries to use a GET request, and Play complains that a GET route does not exist for this method (which is true).
I am solving this by caching the form and defining another method where the form is taken from the cache:
# User data is POSTed to the server
POST /create/insert controllers.MyCreate.insert()
# After a redirect the cached form is displayed again
GET /create/insert controllers.MyCreate.insertGet()
It works, but I don't like this solution. It does not seem normal to have to create another entry in the routes and another method just to adress this problem. I would need to add this hack for every POST route in my application!
Is there a more elegant solution to this?
You could change it into something like this (untested):
def changeLang(lang:String, returnUri:String) = Action {
Redirect(returnUri)
.withCookies(Cookie(LANG, if (lang == "de" || lang == "en") lang else "de"))
}
In you template you would output the route to changeLang in the link, you can get the uri via the request
#routes.Application.changeLang("en", request.uri).url
I suggest you make request implicit in your action and define it as implicit in your template so you don't need to pass it on to each template.
// in the controller
def myUrl = Action { implicit request =>
Ok(views.html.myTemplate("something"))
}
// in the template
#(title:String)(implicit request:play.api.mvc.RequestHeader)
Edit
As for the POST requests, it common (for these types of framework) to have POST requests simple handle stuff and then redirect to another page. The usual flow is like this:
Form submits to a handler
Handler does something with the form information
Handler redirects to a page
An example:
// Hooked up to a GET route
def edit(id:Long) = Action {
// render the view with a form that displays the element with given id
// if the flash scope contains validation information, use that in display
}
// Hooked up to a POST route
def editHandler = Action {
// validate the form
// if validation succeeds
// persist the object
// redirect to edit
// else
// put the form information into the flash scope
// put any validation messages into the flash scope
// redirect to edit
}
If you do not want to use this flow you need to have both a GET and POST route anyway. The user might do a page reload on the resulting page.