Right, if there is no way to do this JS-only without giving away the API token or being able to protect against CSRF this is a no-go.
The standard way to embed a MailChimp form is to use their embedded form. This is actually their recommended option and does not require to publish your API key as far as I can tell.
I was thinking about the router because the site uses the panel and I’d like the panel users to not see these pages (newsletter/subscribe)… and possibly avoid its destruction in the near future by untrained panel users. I’m also not saving any infos there… just communicating with mailchimp.
Is it still doable using the router? I was actually adapting the technic below which @texnixe taught me a while ago. But not sure it’ll work with POST
requests.
c::set('routes', array(
array(
'pattern' => '(:all)/subscribe',
'action' => function($uri) {
tpl::load(kirby()->roots()->templates() . DS . 'newsletter.php', array('uri' => $uri), false );
}
)
));
So all the logic from @bnomei 's wrapper goes in the Uniform action, ok got it.
Then how’d would I call it? Since it will come from several different URLs I can’t use the template controller (the question above). Using the router? Using call_user_func()
?
I’m trying to see the outline of the subscribe action… to understand which script calls what and how to handle the replies from the API.
I see your point… but I have two reasons to use the API:
First is about controlling the subscription experience (custom feedback messages, nice UI, the speed of ajax, use of animation and less bloat from mail chimp (better performance);
Second is about the learning, since I’m new to this I’m trying to learn how to use a API/REST thingy, to figure out the API calls and finally meeting the matrix
Sure, if you don’t need modifiable information (like a target email adress or an API key) for the form, a route is probably the better choice. Here you can find a neat solution using a route as endpoint for an AJAX form (not using Uniform though, but it works the same).
Yes but you don’t mean to copy the logic of the wrapper, do you? You only have to install it according to the readme and use the wrapper in the action. This way you can easily update it in the future.
Even better… coz I was going to copy, slice and hack it till it talks the way I wanted (hahahaha). I just don’t know what I’m doing… which is hard most of the time :-\
Where to put it? Plugins? The readme mentions composer and laravel and stuff…
I’ll study the link and see if can put this together. For now this interaction is very cloudy still, in my mind.
Thanks.
If you don’t want to use Composer you can do it like this:
- Create a new directory
site/plugins/uniform-mailchimp-action
with a fileuniform-mailchimp-action.php
. - Put the
MailChimp.php
file from the wrapper there, too. - Start implementing the action in
uniform-mailchimp-action.php
like this:
<?php
uniform::$actions['mailchimp'] = function ($form, $actionOptions) {
require_once __DIR__.DS.'MailChimp.php';
$mc = new \VPS\MailChimp('yourapikeyhere-us1');
// to be continued ...
};
If you want to use Composer, do this:
- Get Composer, and put the
composer.phar
to the root directory of your Kirby site. - Run
php composer.phar require vatps/mailchimp-rest-api
from a terminal in the same directory. - Add
require __DIR__.DS.'vendor'.DS.'autoload.php';
to theindex.php
. Don’t forget to always upload the newvendor
directory to your server from now on (thecomposer.phar
is only required locally). - Start implementing the new Uniform action as described above but without downloading the
MailChimp.php
and without therequire_once
in the action.
The benefit of using Composer is that you “only” have to run php composer.phar update vatps/mailchimp-rest-api
to update the wrapper. But since this is only a single file and not very likely to receive frequent updates, simply downloading it as described above may be the better option. You can update the wrapper by downloading the new version and replacing the file, too.
Does this get you started?
Martin… you’re awesome!
I followed your advice and didn’t use composer.
Managed to make a test using call_user_func
and now I can see the API response:
dump( call_user_func(uniform::$actions['mailchimp']) );
Is there a better way to test it?
Now I can further implement the Uniform action and make it talk to the frontend via ajax using the previous link you gave me.
Not without adding a bunch of additional complexity (a testing framework) to mock the HTTP requests I guess. The easiest option might be setting up a mailing list and test everything manually
It took me so long getting this to work that someone even marked it as solved…
I ended up mixing technics from @mzur and @luxlogica and got quite confused…
Without the controller method I didn’t know how to configure the Uniform plugin to handle validation… instead of validating “manually” in my custom Uniform::action.
I’d like to not validate it via javaScript… but it seems impossible due to the way the router returns the json.
In summary, is there a way to use Uniform to handle validation? how to initialize it? Above the form in the snippet? in the router? In the custom Uniform::action?
Here’s What I did so for… it works but looks messy…
The config’s router
array(
'pattern' => 'newsletter/subscribe',
'method' => 'POST',
'action' => function() {
// check whether this is an ajax request, and respond with an error if it isn't
if(!kirby()->request()->ajax()){ return response::error("Page Not Found!","404");}
$form_data = kirby()->request()->data();
// add the user and get some feedbacks
return response::json( call_user_func(uniform::$actions['mailchimp'], $form_data) );
}
)
The Uniform action (plugins/uniform-action-mailchimp.php)
uniform::$actions['mailchimp'] = function ($form_data) {
require_once __DIR__.DS.'mailchimp'.DS.'src'.DS.'VPS'.DS.'MailChimp.php';
$mc = new \VPS\MailChimp('xxxxxxxxxxxxxxxxxx-us6');
$list_id = 'xxxxxxxxxxxx';
// get form data
$email = $form_data['_from'];
$potty = $form_data['website'];
$do_website = $form_data['do_website'];
// check if email is valid and the potty is empty
// assemble feedback array (result = [success, message, errors[] ])
$errors = array();
if(!v::email($email)) { $errors[] = 'email'; }
if(!empty($potty)){ $errors[] = 'potty'; }
if(empty($do_website) AND ($do_website !== 'sim') ) { $errors[] = 'do_website'; }
$result = array();
$result['errors'] = $errors;
// if we have validation errors, we can stop and return them:
if(!empty($errors)){
$result['success'] = false;
$result['message'] = 'Validation Failed';
return $result;
}
// if we have no errors, go ahead and insert the email in the list
$response = $mc->post('/lists/'.$list_id.'/members', array(
'email_address' => (string) $email,
'merge_fields' => array('DO_WEBSITE'=>$do_website),
'status' => 'subscribed'
));
// Email already in the list
if ($response['status'] == 400) {
$result['success'] = true;
$result['message'] = 'Thanks, your email was already in the list.';
return $result;
// sloppy way to check if the API returned a pseudo "200" status
} else if (isset($response['id'])) {
$result['success'] = true;
$result['message'] = 'Thank you, soon you\'ll hear from us .';
return $result;
} else {
$result['success'] = false;
$result['message'] = 'Sorry, something went wrong.';
$result['errors'] = 'unknown';
}
// return proper error/success as array
return $result;
};
The js below the form
<script>
// AJAX FORM PROCESSING
$('#newsletter').on('submit', function(e){
e.preventDefault();
var form = $(this);
$.ajax({
type: 'POST',
// use the same url here as the 'pattern' in your route
url: 'newsletter/subscribe',
data: form.serialize(),
success: function(result){
// form data successfully reached form processor api
if(result.success){
// message successfully sent
var msg = "<b>Thank you</b> <br>";
form.find('.form-result').parent().removeClass('hidden');
form.find('.form-result').html(msg += result.message);
} else {
form.find('.form-result').parent().removeClass('hidden');
// an issue was encountered
if(result.errors == undefined || result.errors == null || result.errors.length == 0){
// no validation errors - an email sending error was encountered
var msg = "<b>Ooops</b> <br>";
form.find('.form-result').html(msg += result.message);
} else {
// a validation error was encountered
var msg = "<b>Oops</b> <br>";
if(result.errors.indexOf('email') != -1){
msg += "Please, insert a valid email.";
}
if(result.errors.indexOf('website') != -1){
msg += "We're robots!";
}
form.find('.form-result').html(msg);
}
}
},
error: function(result){
// the form was unable to reach processor api
form.find('.form-result').text('Error '+ result.status + ' - unable to process form: ' + result.statusText);
},
dataType: 'json'
});
});
</script>
You can use Uniform in the route just like you would in a controller:
array(
'pattern' => 'newsletter/subscribe',
'method' => 'POST',
'action' => function() {
// check whether this is an ajax request, and respond with an error if it isn't
if (!kirby()->request()->ajax()) return site()->errorPage();
$form = uniform('newsletter-subscription', [
'required' => [
'do_website' => '',
'_from' => 'email'
],
'actions' => [[
'_action' => 'mailchimp',
'api_key' => 'xxxxxxxxxxxxxxxxxx-us6',
'list_id' => 'xxxxxxxxxxxx',
]]
]);
// get the names of all erroneous fields
$errors = array_keys(array_filter(get(), function ($field) use ($form) {
return $form->hasError($field);
}, ARRAY_FILTER_USE_KEY));
return response::json([
'success' => $form->successful(),
'message' => $form->message(),
'errors' => $errors,
]);
})
This allows you to simplify the action a bit:
uniform::$actions['mailchimp'] = function ($form, $actionOptions) {
require_once __DIR__.DS.'mailchimp'.DS.'src'.DS.'VPS'.DS.'MailChimp.php';
$mc = new \VPS\MailChimp($actionOptions['api_key']);
$list_id = $actionOptions['list_id'];
$response = $mc->post("/lists/{$list_id}/members", [
'email_address' => $form['_from'],
'merge_fields' => ['DO_WEBSITE' => $form['do_website']],
'status' => 'subscribed'
]);
// Email already in the list
if ($response['status'] == 400) {
return [
'success' => true,
'message' => 'Thanks, your email was already in the list.',
];
// sloppy way to check if the API returned a pseudo "200" status
} else if (isset($response['id'])) {
return [
'success' => true,
'message' => 'Thank you, soon you\'ll hear from us.',
];
} else {
return [
'success' => false,
'message' => 'Sorry, something went wrong.',
];
}
}
Note that the do_website
field is not yet validated properly (Uniform only checks if it is not empty). If you tell me what this field is for we can implement a custom validation function for it.
It looks much cleaner now… and using the solid Uniform’s validation… thanks.
The validations is not working thou… when the form sends either valid, invalid emails or an empty input… the json always returns:
{
"success": false,
"message": "",
"errors": []
}
I had to change my router’s pattern because it wasn’t working in grandchildren pages.
Also had to change the ajax url call to include the full caller’s URL:
$.ajax({
type: 'POST',
url: '<?php echo $page->url() ?>/newsletter/subscribe',
data: form.serialize(),
continues...
Here’s my router:
array(
// in 'pattern', enter the same url being called from your ajax javascript function
'pattern' => '(:all)/newsletter/subscribe',
'method' => 'POST',
'action' => function() {
// check whether this is an ajax request, and respond with an error if it isn't
if (!kirby()->request()->ajax()) return site()->errorPage();
$newsletter = uniform('newsletter-subscription', [
'required' => [
'do_website' => 'sim',
'_from' => 'email'
],
'actions' => [[
'_action' => 'mailchimp',
'api_key' => 'xxxxxxxxxxxxxxxxxxxxxxx-us6',
'list_id' => 'xxxxxxxxxx',
]]
]);
// get the names of all erroneous fields
$errors = array_keys(array_filter(get(), function ($field) use ($newsletter) {
return $newsletter->hasError($field);
}, ARRAY_FILTER_USE_KEY));
return response::json([
'success' => $newsletter->successful(),
'message' => trim($newsletter->message()),
'errors' => $errors,
]);
}
)
do_website
is just a string with a value like: "yes"
from a hidden input field… which should be saved to mailchimp.
What about the token? Should I include it? Will it make this form any more secure? Can you find any security flaw in this process?
You always have to include the token with Uniform because forms without a token like that are vulnerable to CSRF attacks. Without the correct token Uniform will ignore all requests and behave as you described.
'required' => [
'do_website' => 'sim',
'_from' => 'email'
],
This can’t work as there is no sim
validator function. If this value is always the same then why do you include it in the form at all? Just set it in your action:
'merge_fields' => ['DO_WEBSITE' => 'sim'],
'pattern' => '(:all)/newsletter/subscribe',
Are you sure this is the right pattern for the route? This will match all routes like home/newsletter/subscribe
, contact/newsletter/subscribe
, error/newsletter/subscribe
, etc. You usually want just one endpoint for a form submission, like api/newsletter/subscribe
. Can you explain why you configured the route like that?
I keep forgetting the basics… my bad, totally
Question: for this form only, is there a way to show custom error messages for email validations (empty or invalid) instead of the default one? I kinda need a different feedback to assist the user. The success ones are ok because they come from the uniform action.
The javascript part of the validations wasn’t working when the request originated from grandchildren pages (i.e.: mysite.com/some-page/sub-page). Hence I also changed the js after the form like…
$.ajax({
type: 'POST',
url: '<?php echo $page->url() ?>/newsletter/subscribe',
continues...
But now I’ll work on that again because everything has changed since…
Tried to do what you’ve said but the problem remains… grandchildren pages gets the following error on submit:
! Fatal error: Uncaught No Uniform actions were given.
thrown in /Users/me/Sites/masite/site/plugins/uniform/lib/UniForm.php on line 126
Yes, the messages are defined in the language files:
l::set('uniform-fields-required', 'Please fill in all required fields.');
l::set('uniform-fields-not-valid', 'Some fields do not contain valid data.');
This affects all forms on the whole site, though. If you have multiple forms and only one of them should have different messages you have to do a little hack: You have to change the language strings in the controller/route just before the uniform()
call and then reset them again afterwards.
And this error doesn’t occur if you use the (:all)/newsletter/subscribe
route? This doesn’t make sense
The route is working properly since Uniform is called but an error like that is only thrown if the actions
array is misconfigured/empty. Did you change anything in the route besides the pattern?
I ended up using jquery to show these different messages… not ideal but it’ll do till I figure out how to do this… because this site isn’t multilang so I’ve modified UniForm.php
to use c::get instead of l::get… and defined those text messages in the config file.
No… it works perfectly.
I also don’t know why.
Absolutely nothing… these subpages/grandchildrem… they have another form that uses Uniform… but I’ve checked their vars and no naming conflict.
Here’s the whole form/jquery and the router:
<section class="newsletter">
<form id="newsletter" method="post">
<input type="hidden" name="_submit" value="<?php echo uniform('newsletter-subscription')->token() ?>">
<label class="hidden form__potty" for="website">Please leave this field blank</label>
<input type="text" name="website" id="website" class="hidden form__potty" />
<div class="hidden" id="feedback">
<div class="feedback-message">Sending...</div>
</div>
<p>
<label class="hidden" for="email">Email *</label>
<input type="email" name="_from" id="email" value="" placeholder="Email" required />
</p>
<p>
<button type="submit">Sign up</button>
</p>
</form>
</section>
<script>
// AJAX FORM PROCESSING
$('#newsletter').on('submit', function(e){
e.preventDefault();
var form = $(this);
var feedback = $('#feedback');
feedback.removeClass('hidden').slideDown('slow');
feedback.find('.feedback-message').html('Sending...');
$.ajax({
type: 'POST',
// use the same url here as the 'pattern' in your route
url: '<?php echo $page->url() ?>/newsletter/subscribe',
data: form.serialize(),
// this success means the ajax call reached the api/action
success: function(response){
if (response.success) { // show messages from mailchimp action
var msg = "<b>Thank you</b> <br>";
msg += response.message;
feedback.find('.feedback-message').html(msg);
form.find('#email, button').prop('disabled', 'disabled');
} else { // a validation error was encountered
var msg = "<b>Oops</b> <br>";
msg += "This email looks wrong.";
feedback.find('.feedback-message').html(msg);
}
},
// the form was unable to reach processor api
error: function(response){
feedback.removeClass('hidden');
feedback.find('.feedback-message').text('An error has occurred.');
},
dataType: 'json'
});
$('html,body').animate({
'scrollTop': $('#newsletter').offset().top
}, 400);
});
</script>
The router:
array(
// in 'pattern', enter the same url being called from your ajax javascript function
'pattern' => '(:all)/newsletter/subscribe',
'method' => 'POST',
'action' => function() {
// check whether this is an ajax request, and respond with an error if it isn't
if (!kirby()->request()->ajax()) return site()->errorPage();
$newsletter = uniform('newsletter-subscription', [
'required' => [
'_from' => 'email'
],
'actions' => [[
'_action' => 'mailchimp',
'api_key' => 'xxxxxxxxxxxxxxxxxxxxx-us6',
'list_id' => 'xxxxxxx',
]]
]);
// get the names of all erroneous fields
$errors = array_keys(array_filter(get(), function ($field) use ($newsletter) {
return $newsletter->hasError($field);
}, ARRAY_FILTER_USE_KEY));
return response::json([
'success' => $newsletter->successful(),
'message' => trim($newsletter->message()),
'errors' => $errors,
]);
}
)
So everything works as expected now?
Yes indeed Martin… again, thanks a lot for all your help.
Wow, epic thread!
The resulting code would make for a great “how-to” post