I am using Razor on ASP.NET MVC with C#.
I am calling an external web page to process a credit card and it returns to me. I then display a receipt.
I'd like to prevent them from going back to the previous screen.
I do not have an underlying cs page, like asp since these are .cshtml files, to grab the event.
This receipt page is a View so I cannot put JavaScript in the header since it would affect every page using it.
Anyone know how I prevent the back button in this circumstance?
One possibility is to exclude the page you don't want to get back to from caching on the client. This could be done by setting the proper response headers. Here's an example with a [NoCache] custom action filter which you could use to decorate the corresponding controller action.
Firstly, if the previous page posted data to the server, best to Redirect(...) to another action after the successful processing, to avoid the data being resubmitted on "Refresh".
Then also set the page to expire, so the back button doesn't work:
https://stackoverflow.com/a/5352953/864763
You're asking the wrong question. Don't try to disable "back" on the client. This will be doomed to fail; you may be able to make it harder, but you'll never win that fight. Instead you should re-write the particular page that you have such that it will only ever process the credit card once. You should (on the server) "remember" that you've processed the credit card so that if the user goes back to the page to resubmit it you can just give them an error message saying "you have already submitted this information, you cannot submit this request twice".
Now, there are several ways of accomplishing this general goal, and some are better than others, but that's the goal you need to strive towards.
One way to do this is to go to every page that will redirect the user to this credit card form; just before submitting the request add something to that user's session (i.e. "pendingCreditCardSubmission" = true) Once they submit that request you then check for that session variable. If it's true, submit the request and set it to false, if it's false or not there then send an error message to the user.
This is how we did it:
public class NoBackFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Response.ExpiresAbsolute = DateTime.Now;
filterContext.HttpContext.Response.Expires = 0;
filterContext.HttpContext.Response.CacheControl = "no-cache";
filterContext.HttpContext.Response.Buffer = true;
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow);
filterContext.HttpContext.Response.Cache.SetNoStore();
filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
if (!filterContext.HttpContext.Request.IsAjaxRequest() && filterContext.HttpContext.Request.HttpMethod != "POST" && !filterContext.Controller.ControllerContext.IsChildAction)
{
var after = filterContext.HttpContext.Request.RawUrl;
var session = GetSession(filterContext);
if (session["Current"] != null)
{
if (session["Before"] != null && session["Before"].ToString() == after)
filterContext.HttpContext.Response.Redirect(session["Current"].ToString());
else
{
session["Before"] = session["Current"];
session["Current"] = after;
}
}
else
{
session["Current"] = after;
}
}
base.OnActionExecuting(filterContext);
}
private HttpSessionStateBase GetSession(ActionExecutingContext context)
{
return context.HttpContext.Session;
}
}
After this you can implement it either in the general scope or in the controller scope.
It has been long since this was asked, but my fix was adding the [NoCache] above the WebPageController class.
[NoCache]
public class WebPageController : Controller
{
public JsonResult JsonError(Exception exception)
{
if (exception == null) throw new ArgumentNullException("exception");
Response.StatusCode = 500;
return new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
success = false,
message = exception.Message,
detail = exception.ToString()
}
};
}
in MVC aspnet framework, you may choose to RedirectToActionPermanent
Which then it tells the browser 301 response code.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.redirecttoactionpermanent?view=aspnetcore-5.0
Related
I have an ASP.NET MVC web site with a large number of Ajax calls to different server-side functions. This site requires a login, so it shouldn't be returning any JSON data if the user isn't logged in. I've already set the site up so that if the user attempts to go to any page or file other than the login page, it returns them to the login page. The problem is that it's doing this with the JsonResponse functions as well, returning the code of the login page when a Json object is expected. I need the Ajax function to recognize the "Not Logged In" error so it can initiate the sending the user back to the login page.
A bit of background, the site does all its database retrieval through a WCF webservice. Whenever a user interacts with the database at all, a last_access DateTime field in the users table is updated. If the user tried to access something and the last_access field shows more than X minutes ago, the webservice throws a "Not Logged In" error back at the web server. I have Application_Error sending them back to the login page with a notice when that happens.
I needed a way to differentiate functions that need to return to the login page from functions that need to return an actual error and function attributes seemed like the easiest way.
Here's an example of one of the JsonResult functions
[HttpPost]
[AjaxTimeoutErrorCheck]
public JsonResult GetRecords() {
ExpDataClient client = new ExpDataClient();
string groups = string.Join(",", SearchSettings.GetGroups().Select(g => g.employerId).ToList());
List<ExpDataService.GetEligibilityResult> items = client.GetEligibility(groups);
JsonResult json = Json(items, JsonRequestBehavior.AllowGet);
json.MaxJsonLength = int.MaxValue;
return json;
}
I have an attribute object set up to handle exceptions in the relevant functions. If I do the following
filterContext.RequestContext.HttpContext.Response.StatusCode = 401;
filterContext.RequestContext.HttpContext.Response.StatusDescription = "No user logged in";
without setting the filterContext.Result, I can get the Ajax function to recognize it as an error, but there is nothing in the return object indicating that it's an error. I suspect that I'm just garbling the response.
I've tried using this https://stackoverflow.com/a/43069917/487909 to create an object to assign to filterContext.Result, but it seems like setting filterContext.Result to anything (other than null, at least) causes the Ajax function to recognize the returned object as valid.
Here's what I currently have for the attribute object
public class AjaxTimeoutErrorCheck : FilterAttribute, IExceptionFilter {
void IExceptionFilter.OnException(ExceptionContext filterContext) {
if (filterContext.Exception.Message == "At least one security token in the message could not be validated.") {
filterContext.ExceptionHandled = true;
var response = new { statusText = "No user logged in" };
filterContext.RequestContext.HttpContext.Response.Status = "401 Unauthorized";
filterContext.RequestContext.HttpContext.Response.StatusCode = 401;
filterContext.RequestContext.HttpContext.Response.StatusDescription = "No user logged in";
filterContext.Result = new Models.JsonHttpStatusResult(response, System.Net.HttpStatusCode.Unauthorized);
};
}
}
If you start a new Web Project, and create a new MVC4 application (with sub-kind as "WebApi", you can paste the below code in (overwriting HomeController.cs) to get the code to work.
I have a MVC4 application (with WebApi).
I am trying to set a custom-header in a MVC controller method and then do a RedirectToAction. The custom-header is not seen in the second mvc-controller-method.
I am able to set a cookie in the first mvc-controller-method and see it in the second mvc-controller-method (after a RedirectToAction).
Is there a way to see the custom-header I set in the second mvc-controller-method after a RedirectToAction ?
Thanks.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
namespace MyMvc4WebApiProjectNamespace.Controllers
{
public class HomeController : Controller
{
private const string CustomCookieName = "CustomCookieName";
private const string CustomHeaderName = "X-CustomHeaderName";
private const string IISExpressRootUrl = "http://localhost:55937/"; /* open up the project properties and go to the web tab and find the iis-express area to get the correct value for your environment */
public ActionResult Index()
{
IEnumerable<string> webApiValues = null;
string value1 = null;
string value2 = null;
HttpClientHandler handler = new HttpClientHandler
{
UseDefaultCredentials = true,
PreAuthenticate = true
};
using (var client = new HttpClient(handler))
{
string valuesUri = IISExpressRootUrl + "api/Values";
webApiValues = client
.GetAsync(valuesUri)
.Result
.Content.ReadAsAsync<IEnumerable<string>>().Result;
if (null != webApiValues)
{
value1 = webApiValues.ElementAt(0);
value2 = webApiValues.ElementAt(1);
}
else
{
throw new ArgumentOutOfRangeException("WebApi call failed");
}
}
HttpCookie customCookie = new HttpCookie(CustomCookieName, "CustomCookieValue_ThisShowsUpIn_MyHomeControllerAlternateActionResult_Method");
Response.Cookies.Add(customCookie);
HttpContext.Response.AppendHeader(CustomHeaderName, "CustomHeaderValue_This_Does_Not_Show_Up_In_MyHomeControllerAlternateActionResult_Method");
//Response.AppendHeader(CustomHeaderName, value2);
return RedirectToAction("MyHomeControllerAlternateActionResult");
}
public ActionResult MyHomeControllerAlternateActionResult()
{
IEnumerable<string> webApiReturnValues = null;
CookieContainer cookieContainer = new CookieContainer();
foreach (string cookiename in Request.Cookies)
{
if (cookiename.Equals(CustomCookieName, StringComparison.OrdinalIgnoreCase))
{
var cookie = Request.Cookies[cookiename];
cookieContainer.Add(new Cookie(cookie.Name, cookie.Value, cookie.Path, "localhost"));
}
}
if (cookieContainer.Count < 1)
{
throw new ArgumentOutOfRangeException("CookieContainer did not find the cookie I was looking for");
}
else
{
Console.WriteLine("This is what actually happens. It finds the cookie.");
}
HttpClientHandler handler = new HttpClientHandler
{
UseCookies = true,
UseDefaultCredentials = true,
PreAuthenticate = true,
CookieContainer = cookieContainer
};
using (var client = new HttpClient(handler))
{
bool customHeaderWasFound = false;
if (null != this.Request.Headers)
{
if (null != this.Request.Headers[CustomHeaderName])
{
IEnumerable<string> headerValues = this.Request.Headers.GetValues(CustomHeaderName);
client.DefaultRequestHeaders.Add(CustomHeaderName, headerValues);
customHeaderWasFound = true;
}
}
/*I wouldn't expect it to be in the below, but I looked for it just in case */
if (null != this.Response.Headers)//
{
if (null != this.Response.Headers[CustomHeaderName])
{
IEnumerable<string> headerValues = this.Response.Headers.GetValues(CustomHeaderName);
client.DefaultRequestHeaders.Add(CustomHeaderName, headerValues);
customHeaderWasFound = true;
}
}
if (!customHeaderWasFound)
{
Console.WriteLine("This is what actually happens. No custom-header found. :( ");
}
string valuesUri = IISExpressRootUrl + "api/Values";
webApiReturnValues = client
.GetAsync(valuesUri)
.Result
.Content.ReadAsAsync<IEnumerable<string>>().Result;
if (null == webApiReturnValues)
{
throw new ArgumentOutOfRangeException("WebApi call failed");
}
}
return View(); /* this will throw a "The view 'MyHomeControllerAlternateActionResult' or its master was not found or no view engine supports the searched locations" error, but that's not the point of this demo. */
}
}
}
Response headers are never copied automatically to requests - so setting any custom headers on response will not impact next request issued to handle 302 redirect.
Note that it is the case even with cookies: response comes with "set this cookie" header, and all subsequent request will get "current cookies" header.
If you have your own client you may be able to handle 302 manually (not possible if you are using browser as client).
As another answer stated, response headers are about this response, not the next one. Redirecting is not a server-side action. A redirect instructs the client to perform a completely new request, and of course in a new request, the response headers for the old request are not present. So return RedirectToAction("MyHomeControllerAlternateActionResult"); is guaranteed to not have this response's headers when the browser initiates the new request.
In trying to solve this problem, one might think of trying to persist the data to the next request server-side, such as through a cookie or in an explicit session variable, or implicitly via use of ViewBag/ViewData/TempData. However, I don't recommend this as using session state heavily has performance implications in large/high-usage web sites, plus there are other negative and subtle side-effects that you may run into down the road. For example, if a person has two browser windows open to the same web site, they can't be doing different actions reliably, as the session data for one window can end up being served to the other one. Avoid session usage as much as possible in your web site design—I promise this will benefit you down the road.
A slightly better way, though still with its problems, is to redirect to a URL with querystring parameters containing a payload. And, instead of the whole set of data, you can provide a key that can be pulled from the session (as long as it's also bound to their IP address and is large like a GUID or two together). However, relying on session state is still not ideal as stated before.
Instead, consider using server-side redirection such as child actions. If you find that hard because what you want to call is a main controller you have a few options:
If you're using dependency injection, add a parameter to the current controller (saving it from the constructor and using it in the request method) that is the desired controller you want to "redirect" to. You can then call that controller directly. This may not be ideal (as all calls to this controller also have to new up a copy of that one), but it does work. Trying to new up the other controller manually can also work, but for reasons I don't fully remember, I think this can give some additional problems. In any case, this method can give issues accessing the HttpRequest context and other context objects correctly, though this can be worked around.
Rearchitect your application so that controllers are not the place where full pages are rendered. Instead, use them as "smart routers" that call child actions to perform the real work. Then, you can call the same child actions from any controller. But this still has problems.
Perhaps the best way is to add custom routing logic through action filters or other means (search the web!) so that the correct controller is hit in the first place! This may not always be possible, but sometimes the need to redirect to another controller mid-procedure actually points to a larger design problem. Focusing on how to cause the knowledge of which controller to hit to be available earlier in the pipeline (such as during routing) can reveal architecture problems, and can reveal likely solutions to them.
There may be other options that I haven't thought of, but at least you have a few alternatives to the simple "no way to do that."
I was able to do something similar like what the user is requesting in the following (rudimentary) way:
In the redirect, add a custom query string parameter
Create a custom Module that checks for that parameter and appends the custom header (read http://dotnetlionet.blogspot.com/2015/06/how-to-add-httpmodule-in-mvc5.html on how to do your own module)
In this way I was able to get my custom headers to be picked up
I am trying to explain why a user was able to submit the same form details twice. At first, I was thinking that the submit button was pushed twice, this may still be the case.
When I checked the results in the database, I can see the same information has been entered twice, but also the same datetime stamp has been entered, down to the second. Surely it takes at least another second to push submit again if this is the case.
In addition after the survey is inputted and saved the user is redirected to a different page.
Why does this happen?
[HttpPost]
public ActionResult InputResult(SurveyViewModel model)
{
if (ModelState.IsValid)
{
Survey_Result InputResult = new Survey_Result();
InputResult.SurveyStatusID = model.SurveyStatusID;
InputResult.Q1DateCompleted = DateTime.Now;
InputResult.Q2 = model.Q2;
InputResult.Q3 = model.Q3;
InputResult.Q10 = model.Q10;
InputResult.Q11 = model.Q11;
InputResult.Q11Other = model.Q11Other;
InputResult.DateAdded = DateTime.Now;
InputResult.AddedBy = Convert.ToInt32(User.Identity.GetUserId());
_surveyService.AddSurvey(InputResult);
_surveyService.Save();
return RedirectToAction("Details", "Survey", new { id = model.SurveyStatusID, feedback = "InputComplete" });
}
return RedirectToAction("Details", "Survey", new { id = model.SurveyStatusID, feedback = "InputError" });
}
The code looks fine to me. If you have access to the user, you could pop Fiddler on it to see if it is posting data twice. If it doesn't happen all the time then its almost certainly user error IMHO.
If you don't have access to the client you could pop in a log entry on each post request or debug line if you are in a position to collect it on this server.
I had similar issues and client side javascript to disable the button on click did the trick for me.
Are you sure you need both AddSurvey and Save?
Remove save and retry.
I have an application that leverages a cookie to support a quasi-wizard (i.e. it's a set of pages that are navigated to by each other, and they must occur in a specific order, for registration).
When the Logon.aspx page is loaded - the default page - the browsers cookies look right.
There's one cookie and it has the right value. This ensures that the next page, which is an enrollment agreement, knows that it was loaded from the Logon.aspx page. However, when I get to that page the browsers cookies look much different:
Now we have two of the same cookie.
This doesn't appear to be causing any real issues - but I can't be sure it won't. So, let me show you the code I'm using to set the cookie (because maybe there's something wrong with it):
if (!this.IsPostBack)
{
Utility.HandleReferrer(Request, Response, "Logon.aspx");
Response.Cookies["lastpage"].Value = "Enroll.aspx";
}
and the HandleReferrer method looks like this:
static public void HandleReferrer(HttpRequest request, HttpResponse response, string expectedReferrer)
{
var cookie = request.Cookies["lastpage"];
if (cookie != null && cookie.Value.ToLower().Contains(expectedReferrer.ToLower()))
{
return;
}
response.Redirect("Logon.aspx");
}
So, why in the world does it duplicate this cookie? It doesn't ever seem to create more than two.
I suggest you do one of the following.
First, get the latest glimpse and try again.
If it is still showing 2 cookies with that name then get firebug and/or fiddler and look at it that way. If I had to take a guess I'd say there's either something wrong in glimpse or something wrong in how you are interpreting the results. Perhaps glimpse is showing what cookies existed before and then after the request was processed?
A third option is to simply emit the cookies collection from your own .net code. Something like:
foreach(HttpCookie cookie in request.Cookies) {
Response.Write(String.Format("{0} = {1}", cookie.Name, cookie.Value));
}
and see what happens.
I tried another approach, by creating a method that will return the latest cookie occurrence, this way I'll always get the right data.
This method expect the collection of cookies from Request, and the name of the searched cookie, and returns the latest ticket (the information that is normally encrypted)
private static FormsAuthenticationTicket GetLatestCookie(HttpCookieCollection cookies, string cookieName) {
var cookieOccurrences = new List<FormsAuthenticationTicket>();
for (int index = 0; index < cookies.Count; index++) {
if (cookies.GetKey(index) == cookieName) {
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookies[index].Value);
cookieOccurrences.Add(ticket);
}
}
DateTime oldestTime = DateTime.MinValue;
FormsAuthenticationTicket oldestTicket = null;
foreach (var formsAuthenticationTicket in cookieOccurrences) {
if (formsAuthenticationTicket.Expiration > oldestTime) {
oldestTime = formsAuthenticationTicket.Expiration;
oldestTicket = formsAuthenticationTicket;
}
}
return oldestTicket;
}
In my C# .NET 4 MVC 3 application I have a delete controller for a set of CRUD pages which uses the Post Redirect Get pattern to redirect to an Index controller after a successful delete. I would like to render a button on the Index page only if this page was NOT redirected to by such an action. Is there a simple way to detect if the current page was redirected to (i.e. was reached as the result of a PRG redirect)?
After reading http://blog.simonlovely.com/archive/2008/11/26/post-redirect-get-pattern-in-mvc.aspx my current approach is to set this in my delete controller with TempData after the DeleteMyEntity method has succeeded:
try {
MyService.DeleteMyEntity(MyViewModel.MyEntity);
TempData["Redirected"] = true;
args = new RouteValueDictionary(new { Foo = 1, Baa = 2 });
return RedirectToAction("Index", args);
} catch (Exception e)
{
//Logging etc. - redirect should never be reached on exception (and TempData item not set)
throw(e);
}
then in my Index controller I check to see if this value exists and is true:
if (TempData["Redirected"] != null)
{
//we can then do something useful with this
}
Another opportunity I see would be to add another item to args and check for this in the controller, but in this case I may as well just use TempData. Is there a way to do this using a HTTP Response code on the request without needing to pass this data through with TempData or a similar mechanism?
another route would be to set up a global actionfilter that "injects" that flag for you...
public class RedirectDetect: ActionFilterAttribute{
public override void OnActionExecuted(ActionExecutedContext filterContext){
if (filterContext.Result is RedirectToRouteResult ||
filterContext.Result is RedirectResult)
{
TempData["Redirected"] = true;
//or what ever other indicator you want to set
}
}
}
And then you can just call redirectToAction("Index") and then check in your receiving handler
sidenote: I challenge you to say RedirectDetect aloud and not smirk.
I use TempData in a similar fashion - for instance, to show a status message (after redirecting to) my view when a record has been added / updated / deleted. This is the kind of simple, throw-away stuff that TempData is used for, so I say what you have is appropriate.
Personally I wouldn't mess with HTTP status codes unless I had an absolute need for it. And you could probably do something with the referrer http header, but again, that would be much messier and more complicated than just using TempData. You have a clean, simple solution that works, I say go with what you have.
I am not aware of any simpler mechanism and have been using TempData for quite some time to implement Post-Redirect-Get features. As far as I know, this is specifically one of the reasons for TempData's existence. I would continue using it.
There is no way to tell the difference between requests as a result of a 3xx redirection or a straightforward user-initiated GET. The best way is to provide a querystring argument that is only appended by the redirection for the initial POST request, but there is nothing stopping the user from reloading the page with the same querystring.
Hmmm, or you could send a cookie with the redirection from the POST, then remove the cookie in the response of the subsequent GET, like so:
public ActionResult PostHandler(ViewModel model) {
SetCookie("redirected", "true"); // psuedocode
return Redirect("GetHandler2");
}
public ActionResult GetHandler2() {
if( GetCookie("redirected") == "true" ) {
// do something
}
DeleteCookie("redirected");
}
Building off of George's answer:
public class MarkRedirects : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is RedirectToActionResult ||
context.Result is RedirectResult)
{
// Obtain and verify the underlying IController
var controller = context.Controller as Controller;
if (controller != null)
controller.TempData["Redirected"] = true; // or set other dictionary data here
}
}
}
The conditional checks of context.Result can vary based on what method you used to redirect, for instance if you redirected the user via the RedirectToAction() method, context.Result is RedirectToActionResult will return true but context.Result is RedirectToRouteResult will not.
Because of this, you will want to change up that conditional based on your personal taste of how you redirect users. The current code would work for OP's situation.
If you're going to be using this everywhere, it may be wise to modify a base controller.