U4-1639 - Add support to generate GET urls to SurfaceController actions

Created by Matt Brailsford 04 Feb 2013, 13:34:00 Updated by Sebastiaan Janssen 17 Feb 2014, 17:03:52

Is duplicated by: U4-2748

Currently, to hit a surface controller action, it needs to be done via a POST request, however, it would he handy to be able to generate GET urls, such as, when looping through a list of items, we might want to have a delete link next to them which links to a surface controller to perform the action. Right now, this has to come from a form post, but as I say, would be handy if this could be done from a GET perspective.

From the looks of it, the handler for surface controllers is looking at Request[...] for it's values, so it should just be a case of creating a Html helper to generate the links.

Comments

Matt Brailsford 04 Feb 2013, 13:35:12

Ooops, would probably be better to hand off the Url helper, but you get what I mean :)


Sebastiaan Janssen 28 Aug 2013, 08:04:46

You can do this by adding the [HttpGet] attribute on the action.

Example: public class LogoutController : SurfaceController { [HttpGet] public ActionResult HandleLogout() { //Test if the member is logged in, if so remove from cache if (Member.IsLoggedOn()) { var memberId = Member.CurrentMemberId(); Member.RemoveMemberFromCache(memberId); Member.ClearMemberFromClient(memberId); }

        return Redirect("/");
    }
}

URL: /Umbraco/Surface/Logout/HandleLogout

If there's any paramters defined on the action you can add them in querystring variables. Please re-open and clarify if this is not what you meant.


Sebastiaan Janssen 30 Aug 2013, 08:14:25

Ah! I get the issue now, the BeginForm method generates a form method="post" but you also want to be able to generate form method="get". Why didn't you just say so! ;-)


Shannon Deminick 05 Sep 2013, 03:42:22

I have this working but have a question. When we submit the action with Get, we end up with a long URL because we need to add our encrypted query string parameter in order to wire up the current umbraco page after the custom action executes. So the URL might look something like:

http://localhost:6200/?Name=hello&Email=world&uformpostroutevals=1A0A2F4F0D341628EC85F2D1A29D42F5021E5C9266A05372B010AA30FCE0E8791723528E8CE4AD9152A82CEC874188F669E5133570E1DDE38AA0344583561EA76163816F96849B0058B64C67748AED6E641AC0A11AC2753C6C00CD78EFC1D6E7DB34463FE4862329471498D30D81FCBCDC9039393F61C48E3B52350DF59AD146174019A3164B62279C4BA53007A5299E996E7729DB08A3BEA46FEA17417C44D46C66E5ECA12A38A7E4B85C8B11192446

That encryption key won't really get much longer than that so we really shouldn't have to worry too much about exceeding IE's max query string limit... but it is pretty ugly.

So we have 3 options:

which is shorter, we have to put a GUID in there to reference the correct cookie since people could have multiple tabs open. Once the cookie is read after submitting the form it is removed. But if someone doesn't submit the form they end up with a stale cookie for 1 day (or whatever). So for that reason any time a call to BeginUmbracoForm is made we'll clear all user cookies that have our cookie key prefix.

  • We use a temporary session storage with basically the same process and the URL would look the same. (We cannot use TempData - which is session anyways - because when we need the data we don't have a controller yet and TempData requires a controller + controller context).

What do you guys think? If we wanted we can have a custom FormAction enum. Currently it is POST, GET but we could have Post, Get, GetWithId ?


Shannon Deminick 05 Sep 2013, 03:42:54

(make sure you scroll in the above comment to see how long the original url is)


Shannon Deminick 05 Sep 2013, 03:45:42

...actually for the cookie way, we won't set an expiration date on it, it will just expire at the end of the session, since we clear these cookies anytime BeginUmbracoForm is rendered we won't end up with multiple stale cookies.


Shannon Deminick 05 Sep 2013, 03:58:07

I've implemented this with the cookie solution and it is working well. I've also changed the post/get parameter from the name "uformpostroutevals" to "ufprt". This just makes the URL slightly shorter but is also unique enough that nobody will have a parameter with that name.


Shannon Deminick 05 Sep 2013, 03:58:48

So the URL looks like this:

http://localhost:6200/?Name=asdfasdf&Email=45345345&ufprt=8195ad8904964ab9bcb064b6cbdcdfc1


Shannon Deminick 05 Sep 2013, 04:11:52

rev 177486418ded48de2f2411c090a517dc8822e743


Matt Brailsford 05 Sep 2013, 07:56:00

Weird, seems i haven't been getting updates on issues I reported.

Thanks for looking into this guys, but the route I was meaning was more like Sebs demo, where by I ultimately want to generate a with a link to a surface controller action (ie like the delete link I mention in my example) that can just perform a task and redirect back with a message. In this instance, I wouldn't want to use a form at all, instead just have a complete link for someone to follow.

Ultimately I guess i'm thinking along the lines of:

@Url.SurfaceAction("Action", "Controller")

Or something, where the ufprt is pre filled in so that the surface controller maintains context.

Does that make sense?

Cheers

Matt


Shannon Deminick 05 Sep 2013, 08:34:49

Well, you can either wrap your button in a BeginUmbracoForm and don't use an anchor and instead a button, or use an anchor that submits the form.... OR (since that sux :)...

You can hijack a route to achieve what you want. If you navigate to a page with query strings in it and you hijack a route then you have access to the query strings and model binding.

I think i understand your idea about this: @Url.SurfaceAction("Action", "Controller") but you still need to specify an actual page that you want to go to, or do you just want to go to the current page with query strings ?

Really not sure why you'd want to delete something with a GET request... then anyone can over and over again just delete stuff.


Matt Brailsford 05 Sep 2013, 09:09:58

Hey Shan,

I see Url.SurfaceAction working like BeginUmbracoForm in that it takes the current page as the context, but just outputs a url with everything it needs that can be used as the source to an a-href.

I don't see how a GET request is any less reasonable than a POST request. Someone can easily hammer both. The main thing I wanted to get away from was that if I wanted to perform a server action as a result of a front end click WHILST maintaining context, I din't want to have to pepper the front end code with a bunch of forms and have to use html buttons to submit them, when it could easily be done with a a-href.

That any clearer?

Cheers

Matt


Shannon Deminick 06 Sep 2013, 00:29:55

So essentially what you want is just a URL generator to link to any given page, or the current page that will auto-bind like a GET form to a surface controller? ... I can make that happen. But please note that you can easily achieve the desired outcome with hijacking routes without having an additional special query string.

I've realized however that we cannot use the cookie method so we have to end up with long ugly query strings because if someone bookmarks the URL it will be stale and a YSOD will get thrown. So we're gonna be stuck with the long URLs unfortunately.


Shannon Deminick 06 Sep 2013, 01:40:14

Ok well I've got this working but the problem is that the encrypted string is dynamic, meaning it always changes with each encryption even though the values are the same (unlike a hash). So even though it will decrypt any of the encrypted keys it will generate if you site is indexed by Google or another search engine you will have a new link for every link for every visit.... This is a reason to not have links for form actions, search engines will index your actions :) (though perhaps you can add to ignore on robots.text or add a no-follow attribute).

I can create an overload so the URL generated for the SurfaceAction are in base64 but of course there can be no sensitive data in the additionalRouteParams because it will not be encrypted.

Let me know if you care about that, for now I'm just leaving it as an dynamic encrypted string as per BeginUmbracoForm.


Shannon Deminick 06 Sep 2013, 01:43:12

Done in rev: fee02af4eb9c7da5f20669c3eec78d0658823409

So here's a challenge for you @Matt! -

I've completed the feature request (actually two feature requests) for Url.SurfaceAction and Html.BeginUmbracoForm with a GET so now I challenge you to write the documentation for these :) ? Are you game ? Perhaps could go here: http://our.umbraco.org/documentation/Reference/Mvc/forms


Matt Brailsford 06 Sep 2013, 07:45:06

Hey Shan,

I'm not too bothered about the dynamic encrypted string, the url is what the url is. You can always use a canonical meta tag or the likes if it causes any SEO issues.

RE the docs, I'll see what I can do later today :)

Great work fella.


Shannon Deminick 06 Sep 2013, 07:46:59

Cool sounds good!


Jeroen Breuer 17 Feb 2014, 16:49:25

If everything that will be in 6.2.0 also in 7.1? Because 7.1 now comes out before 6.2.0 this is a bit confusing.


Sebastiaan Janssen 17 Feb 2014, 17:03:52

Yes.


Priority: Normal

Type: Feature (request)

State: Fixed

Assignee: Shannon Deminick

Difficulty: Easy

Category:

Backwards Compatible: True

Fix Submitted:

Affected versions:

Due in version: 7.1.0, 6.2.0

Sprint:

Story Points:

Cycle: