Kendo Server Side Validation
Mar 1 |
One of the reasons that there are very few posts concerning server-side validation with the Kendo validator is that it is not really built to do this. Unlike the majority of the other Kendo widgets which allow for customization, the validator was meant for simple validation. The built-in validation is quite useful for simple client-side validation, but it is not an extensive validation library and anytime that you need to extend it you will find yourself wanting more. I felt like I was trying to hammer a square peg into a circle while coding this. However, since one of the main goals of this blog is to share how ColdFusion can use the Kendo UI, I felt the need to dig into the kendo validator. I have a captcha form on this blog that is used to verify that the user is an actual user, and it encrypts a token and passes it off to the server-side for validation. You can see this in action by making a comment on this post below. The meat and potatoes of this function, like most of the other Kendo widgets, lie in Javascript. This script is heavily commented on.
$(document).ready(function() {
// Validation.
// Preset our sessionStorage var. This is set to '' initially to indicate that server side validation has not yet occurred.
sessionStorage.setItem("captchaValidated", "");
// Set the initial value of the captchaValidatedValue form element. We need to store this in order to know when to hit the server with a new validation request. We don't want to hit the server 3 times a second unless the text value has actually changed.
sessionStorage.setItem("captchaValidatedValue", "");
// Since the kendo validator occurs so quickly, it may send an erroneous value to the server the a few times before it picks up the new value that was entered. We need to allow several attempts to occur when we hit the server. This is a numeric value that will be incremented.
sessionStorage.setItem("captchaValidatedAttempts", "0");
// Invoked when the submit button is clicked. Instead of using '$("form").submit(function(event) {' and 'event.preventDefault();', We are using direct binding here to speed up the event.
var addCommentSubmit = $('#addCommentSubmit');
addCommentSubmit.on('click', function(e){
// Prevent any other action.
e.preventDefault();
// Set the attempts var to 0
sessionStorage.setItem("captchaValidatedAttempts", 0);
// Note: when using server side logic, this function may not post the data to the server due to the time required to return the validation from the server.
// If the form has been successfully validated.
if (addCommentFormValidator.validate()) {
// Submit the form. We need to have a quick timeout function as the captcha resonse does not come back for 150 milliseconds.
setTimeout(function () {
// Note: when testing the ui validator, comment out the post line below. It will only validate and not actually do anything when you post.
postCommentSubscribe(<cfoutput>'#URL.Id#'</cfoutput>, <cfoutput>'#URL.uiElement#'</cfoutput>);
}, 300);//..setTimeout(function () {
}//..if (addCommentFormValidator.validate()) {
});//..addCommentSubmit.on('click', function(e){
// !!! Note on the validators, all forms need a name attribute, otherwise the positioning of the messages will not work. Also data attributes that are dash separated become camel cased when retrieved using jQuery. --->
addCommentFormValidator = $("#addCommentSubscribe").kendoValidator({
// Set up custom validation rules
rules: {
// Name of custom rule.
// This can be any name, but I typically put the name of the field and a verb to indicate what I am enforcing ('nameIsRequired'). Note: if you just want to check to see if something was entered you can specify 'required' in the form element.
// This rule is quite different as it relies upon server side processing. I used https://www.telerik.com/blogs/extending-the-kendo-ui-validator-with-custom-rules as an example to build this.
captcha:
function(input) {
if (input.is("[id='captchaText']")){
// The captchaValidated value is set in storage session and set in the function below. Note, until the form loses focus, this function is constantly being validated until validation passes. Be careful not to go into an endless loop without exits.
var captchaValidated = getCapthchaValidated();
// If the captcha has not been validated on the server...
if (captchaValidated == ''){
// Check the captcha
captchaText.check(input);
// And stop...
return false;
}
// If the server validation failed, try again...
if (captchaValidated == 'no'){
// Check the captcha
captchaText.check(input);
// And stop...
return false;
}
if (captchaValidated == 'yes'){
// The captha text was succuessfully validated. Exit this function.
return true;
}
}//..if (input.is("[id='captchaText']")){
// This rule does not apply to the captha text input.
return true;
}//..function(input) {
}
//..captcha:
}).data("kendoValidator");
// Create a variable for this function as we will use the properties in the captch validation function above when it returns results.
var captchaText = {
check: function(element) {
// Note: the validator will fire off a new request 3 times a second, and we need to make sure that we are not hitting the server with stale data every time. We are going to see if the value has changed before firing off a new request to the server.
// Compare the input value to the value that was stored in sessionStorage. If the data has changed, and there has been fewer than 5 validation attempts that have failed, hit the server.
if (element.val() != getCapthchaValidatedValue() || getCaptchaValidatedAttempts() <= 5){
// Post to the server side method that will validate the captcha text.
$.ajax({
url: "<cfoutput>#application.proxyController#</cfoutput>?method=validateCaptcha",
dataType: 'json', // Use json for same domain posts. Use jsonp for crossdomain.
data: {
// Send in the arguments.
captchaText: element.val(),
captchaHash: $( "#captchaHash" ).val()
},
success: function(data) { // The `data` object is a boolean value that is returned from the server.
var captchaValidated = getCapthchaValidated();
if (data){
// debugging alert('Yes!');
// Set the value on the cache object so that it can be referenced in the next validation run. Note: sessionStorage can only store strings.
sessionStorage.setItem("captchaValidated", "yes");
// At the tail end of the validation process, when the validated data is complete, post the data. Since we have passed validation, we don't need to hit the 'captcha' custom rule above again.
if (addCommentFormValidator.validate()) {
// Hide the custom window message
kendo.ui.ExtAlertDialog.hide;
// submit the form. We need to have a quick timeout function as the captcha resonse does not come back for 150 milliseconds.
setTimeout(function () {
// Note: when testing the ui validator, comment out the post line below. It will only validate and not actually do anything when you post.
postCommentSubscribe(<cfoutput>'#URL.Id#'</cfoutput>, <cfoutput>'#URL.uiElement#'</cfoutput>);
}, 300);//..setTimeout(function () {
}
} else {
// Get the number of validation attempts.
var captchaValidatedAttempts = getCaptchaValidatedAttempts();
// Increment the validation attempt.
var currentCaptchaValidatedAttempt = (captchaValidatedAttempts + 1);
// Store the number of validation attempts in sessionStorage.
sessionStorage.setItem("captchaValidatedAttempts", currentCaptchaValidatedAttempt);
// After the 5th bad attempt, set the validation var and use a quick set timeout in order for the data to come back and be validated on the server before launching our custom error popup. Otherwise, if there was a previous captch error from the server, this custom error will pop up as the new data has not had a chance to be returned from the server yet.
if (currentCaptchaValidatedAttempt == 6){
// Store that we tried to validate, but it was not correct.
sessionStorage.setItem("captchaValidated", "no");
// Load a new captcha image (this is my own custom requirement and it has no bearing to the validator logic).
reloadCaptcha();
// Popup an error message.
setTimeout(function() {
if (getCapthchaValidated() == 'no'){
// Note: this is a custom library that I am using. The ExtAlertDialog is not a part of Kendo but an extension.
$.when(kendo.ui.ExtAlertDialog.show({ title: "The text did not match", message: "We have reloaded a new captcha image. If you're having issues with the captcha text, click on the 'new captcha' button to and enter the new text.", icon: "k-ext-warning", width: "<cfoutput>#application.kendoExtendedUiWindowWidth#</cfoutput>", height: "215px" }) // or k-ext-error, k-ext-question
).done(function () {
// Do nothing
});//..$.when(kendo.ui.ExtAlertDialog.show...
}//..if (addCommentFormValidator.validate()) {
}, 500);// A half of a second should allow the server to validate the captcha and return the result.
}
}
// Store the validated value. We will use this to determine when to hit the server for validation again if the value was not correctly typed in.
sessionStorage.setItem("captchaValidatedValue", element.val());
// Trigger the validation routine again. We need to validate each time, even if the value is validated on the server as we need to eliminate the error message raised in the validation script and will be popped up when the form loses focus on the onBlue event.
setTimeout(function() {
addCommentFormValidator.validate();
}, 2000);// Wait 2 seconds to hit the server again.
}//..success: function(data) {
// Notes: success() only gets called if your webserver responds with a 200 OK HTTP header - basically when everything is fine. However, complete() will always get called no matter if the ajax call was successful or not. It's worth mentioning that .complete() will get called after .success() gets called - if it matters to you.
});//..$.ajax({
}//..if (element.val() != getCapthchaValidatedValue()){
}//..check: function(element, settings) {
};//..var captchaText = {
});//...document.ready
// Validation helper functions. These must be oustide of the document ready block in order to work.
// Note: due to the latency of the data coming back from the server, we need to have two points to post a completely validated form to the server for processing. The first point is when the user clicks the submit form button, and the second point is at the tail end of the processing when the server has validated data.
// I am using sessionStorage to store the value from the server in order to effect the captach widget that I developed. I don't want to have to ask the user to go thru the captha validation process multiple times within the same session and don't want to have to write out the logic every time.
function getCapthchaValidated(){
return sessionStorage.getItem("captchaValidated");
}
// Prior to validation, what did the user enter?
function getCapthchaValidatedValue(){
// Since sessionStorage only stores strings reliably, this will be either: '', 'no', or 'yes'.
return sessionStorage.getItem("captchaValidatedValue");
}
// Returns the number of attempts that the server tried to validate the data. This only gets incremented when the server comes back with a false (not validated).
function getCaptchaValidatedAttempts(){
var attemps = sessionStorage.getItem("captchaValidatedAttempts");
return(parseInt(attemps));
}
Server-side ColdFusion:
5) The server-side logic determines if the text that the user entered matches the text that is shown in the captcha image.
5a) Does the text match the captcha image? Will return a boolean value (true/false).
5b) We need to eliminate any chance that a positive result is not overwritten. The client is firing off server-side ajax requests 3 times a second, and we need to be careful not to allow a subsequent ajax request to overwrite our value. We are using a server-side cookie to ensure that this does not happen.
<!--- 5) Helper functions for interfaces (addComments, addSub, etc.). Important note on function tags- they must have a returnFormat="json". Otherwise, ColdFusion will return the value wraped in a wddx tag.--->
<cffunction name="validateCaptcha" access="remote" returnType="boolean" returnFormat="json" output="false" hint="Remote method accessed via ajax. Returns a boolean value to determine if the users entered value matches the captcha image.">
<cfargument name="captchaText" required="yes" hint="What did the user enter into the form?" />
<cfargument name="captchaHash" required="yes" hint="The hashed value of the proper answer. This must match the captcha text in order to pass true." />
<cfargument name="debugging" required="no" type="boolean" default="false" hint="For testing purposes, we may need to not use the session.captchValidated value to prevent a true value from being incorreclty reset." />
<!---5a) Does the text that the user entered match the hashed value?--->
<cfif application.captcha.validateCaptcha(arguments.captchaHash,arguments.captchaText)>
<cfset captchaPass = true />
<!--- Set the captcha validated cookie to true. It will expire in one minute. --->
<cfcookie name="captchaValidated" expires="#dateAdd('n', 1, now())#" value="true">
<cfelse>
<!--- 5b) Note: the captcha will only be validated true one time as the encryption tokens get changed on true. However, the kendo validator validates quickly on blur, so there many be a true value overwritten by a false a millisecond later. We don't want to ever change a true value to false and will use session vars to prevent this behavior. You can override this behavior by setting debugging to true. --->
<cfif not debugging and isDefined("cookie.captchaValidated")>
<cfset captchaPass = true />
<cfelse>
<cfset captchaPass = false />
</cfif>
</cfif>
<!---Return it.--->
<cfreturn captchaPass />
</cffunction>
The HTML is rather simple, the key here are the custom messages are displayed in the 'data-required-msg="Captcha text is required."' and 'data-captcha-msg="The text does not match."'. These tags will pop up the required message when the captcha text has not been filled out, and when the text that the user has entered does not match the text in the captcha image. I am not dealing with any other custom messages here. The rest of the code does not apply, but I am including it for reference.
HTML:
6) The captcha HTML input.
<!-- Captcha -->
<tr height="35px" class="k-alt">
<td>
<!--- Captcha logic in its own table. This is a Kendo Mvvm template. --->
<div id="captchaImage" class="container k-alt">
<table align="left" class="k-alt" width="100%" cellpadding="0" cellspacing="0">
<!--- The source refers to the javascript code that will be used to populate the control, the template is the UI and it is not associated with the javascript code. --->
<tbody data-bind="source: captchaTextObj" data-template="captchaTemplate" data-visible="true"></tbody>
</table>
<!--- Create a Kendo template. We will use this to refresh the captcha hash and image on the page.--->
<InvalidTag type="text/x-kendo-template" id="captchaTemplate">
<tr class='k-alt'>
<td><label for="captchaText">Enter image text:</label></td>
</tr>
<tr class='k-alt'>
<td>
<input type="hidden" id="captchaHash" name="captchaHash" value="#: captchaHashReference #" />
<!--- 6) Create the captcha input with the custom messages. --->
<input type="text" name="captchaText" id="captchaText" size="6" class="k-textbox" style="width: 250px"
placeholder="Enter Captcha Text" required
data-required-msg="Captcha text is required."
data-captcha-msg="The text does not match." />
</td>
</tr>
<tr class='k-alt'>
<td>
<img src="#: captchaImageUrl #" alt="Captcha" align="left" vspace="5" border="1" />
</td>
</tr>
<tr class='k-alt'>
<td>
<button type="button" class="k-button" onClick="reloadCaptcha()">
<i class="fas fa-redo" style="alignment-baseline:middle;"></i> New Captcha
</button>
</td>
</tr>
</script>
</div><!---<div id="captchaImage" class="container">--->
</td>
</tr>
Tags
Kendo UIThis entry was posted on March 1, 2019 at 7:34 PM and has received 3131 views.