ControllerWrapper changing the request HTTP verb when invoki

Posted by jacopo furla on 19-Nov-2019 17:10

Hello, we're upgrading from 10.2 to 12.2 and we're experiencing a blocking problem.

In many pages we have custom widgets that post forms back to themselves that have stopped working after the upgrade.

After extensive debugging we found out that when these widgets are placed in a detail page (a page showing the details of an entry of a list) the Feather system is calling the protected override void HandleUnknownAction(string actionName) method of the controller of the widget because it thinks that the url-name of the item that is appended to the URI is the action invoked.

E.g. We have a page called "Foo" which displays the details of an item selected from a list. The URI of the page when displaying an item is /foo/hello-world where /foo is the URI of the page and hello-world is the URL name of the item. The Feather system when calling the controllers of the widgets placed in the Foo page is passing hello-world as the action.

The problem lies in how the HandleUnknownAction method is called.

The HandleUnknownAction method is called by the public void CallHandleUnknownAction(string actionName) method in Telerik.Sitefinity.Mvc.ControllerWrapper, in this method a new HTTP context is created, forcing its HTTP verb to GET:

public void CallHandleUnknownAction(string actionName)
{
  if (AjaxRequestExtensions.IsAjaxRequest(this.controller.Request))
  {
    return;
  }
  
  MethodInfo methodInfo = ((IEnumerable<MethodInfo>) this.controller.GetType()
    .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic))
    .Where<MethodInfo>((Func<MethodInfo, bool>) (m => m.Name == "HandleUnknownAction"))
    .SingleOrDefault<MethodInfo>();
  
  if (!(methodInfo != (MethodInfo) null) || !(methodInfo.DeclaringType != typeof (Controller)))
  {
    return;
  }
  
  using (new HttpMethodRegion(this.controller.ControllerContext, "GET"))
  {
    methodInfo.Invoke((object) this.controller, new object[1]
    {
      (object) actionName
    });
  }
}

This is effectively changing every request to a GET request, regardless of which verb has been used by the browser.

Since our controllers have their action bound to the POST verb — using the [HttpPost] attribute — all these requests can no longer be handled.

We couldn't find anything about this in the release notes, but this problem is blocking our upgrade since we lose a good chunk of features.

Is changing the HTTP verb a conscious decision by Sitefinity or a bug?

How can we correctly POST forms in details pages?

All Replies

Posted by jread on 19-Nov-2019 19:04

How are your custom widgets created? Are they WebForms or MVC?  If MVC in the controller you should handle unknown routes per controller.

protected override void HandleUnknownAction(string actionName)
		{
			this.ActionInvoker.InvokeAction(this.ControllerContext, "Index");
		}

Also if the widgets are MVC and your are using the @Html.BeginForm() you should be using @Html.BeginFormSitefinity() instead.

@using Telerik.Sitefinity.Frontend
@using Telerik.Sitefinity.Frontend.Mvc.Helpers
@using Telerik.Sitefinity.Frontend.Mvc.StringResources
@using Telerik.Sitefinity.UI.MVC;


@using (Html.BeginFormSitefinity("Change", "TemplateCategoryChanger", FormMethod.Post, new { role = "form", }))
	{ @*FORM STUFF*@}

Posted by jacopo furla on 20-Nov-2019 09:50

[quote user="jread"]

How are your custom widgets created? Are they WebForms or MVC?

[/quote]

These are MVC widgets.

[quote user="jread"]

If MVC in the controller you should handle unknown routes per controller.

[/quote]

We do have the HandleUnknownAction method in our controller, that's the problem.

It's when the HandleUnknownAction method is called that the request gets changed from POST to GET.

[quote user="jread"]

Also if the widgets are MVC and your are using the @Html.BeginForm() you should be using @Html.BeginFormSitefinity() instead.

[/quote]

From what I can see we're not using neither the Html.BeginForm nor the Html.BeginFormSitefinity helpers because we're not using custom forms.

In the back-end the widget allows the users to choose one of the native forms available in Sitefinity (the ones created in Content > Forms), these forms are then rendered in the page directly by Sitefinity, so we can't change how the <form> element is constructed.

Posted by jread on 21-Nov-2019 13:16

Is there anymore information you can provide how these routes are called? are you using absolute paths, Action results to generate the route links?

Posted by jacopo furla on 22-Nov-2019 10:19

This is an extremely gutted down example of how the custom widgets are structured:

using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web.Mvc;

using Newtonsoft.Json.Linq;

using Telerik.Sitefinity.Frontend.Forms.Mvc.Models;
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers;
using Telerik.Sitefinity.Frontend.Resources;
using Telerik.Sitefinity.Mvc;
using Telerik.Sitefinity.Services;

namespace SitefinityWebApp.Mvc.Controllers
{
    [ControllerToolboxItem(Name = "MyWidget", Title = "My Widget", SectionName = "My Sections")]
    public sealed class MyWidgetController : Controller
    {
        private IFormModel formModel;

        [HttpGet]
        public ActionResult Index()
        {
            if (SystemManager.IsDesignMode && this.FormModel.FormId == Guid.Empty)
            {
                return this.Content("<p style='text-align: center;'>Select a form.</p>", "text/html", Encoding.UTF8);
            }

            var packageManager = new PackageManager();
            var packageName = packageManager.GetCurrentPackage();
            var formId = this.FormModel.FormId.ToString("D", CultureInfo.InvariantCulture);
            var viewPath = "~/Mvc-Form-View/" + packageName + "/" + formId + ".cshtml";
            var viewModel = this.FormModel.GetViewModel();

            return this.View(viewPath, viewModel);
        }

        [HttpPost]
        public ActionResult Index(FormCollection formCollection)
        {
            return this.Redirect("/");
        }

        [Browsable(false)]
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public IFormModel FormModel
        {
            get
            {
                var type = this.GetType();

                this.formModel = ControllerModelFactory.GetModel<IFormModel>(type);

                if (!string.IsNullOrWhiteSpace(this.SelectedItems))
                {
                    var id = JArray.Parse(this.SelectedItems).Single().Value<string>("Id");

                    if (Guid.TryParseExact(id, "D", out var formId))
                    {
                        this.formModel.FormId = formId;
                    }
                }

                return this.formModel;
            }

            set
            {
                this.formModel = value;
            }
        }

        public string SelectedItems { get; set; }

        protected override void HandleUnknownAction(string actionName)
        {
            this.ActionInvoker.InvokeAction(this.ControllerContext, nameof(this.Index));
        }
    }
}

I don't exactly know how the routes are constructed, since we only render the cshtml Sitefinity generates for the forms.

This thread is closed