Repository: django-hijack/django-hijack

Issue:
Admin integration creates invalid HTML
The new admin integration uses a form. Sadly, Django wraps the whole change list table inside a form for the change list form actions. All versions of HTML and XHTML forbid nested forms.

I see two possible solutions.
1. We use a GET request, including the CRSF token.
2. We use the actions of a button in Django's admin. However, that could be counterintuitive, since one could select multiple users.
3. We use the change list action view and "hack it". We would only have a single form in the DOM. However, that would make object based next values impossible.

@Mogost what do you think?


Discussion:
> We use the change list action view and "hack it".

Think that's sensible, a hybrid approach of using the actions framework to piggyback on the POST/CSRF logic but then not exposing it as an action in the drop down. So if hijack button is just something simple like:

```django
<button type="submit" name="hijack_user" value="{{ obj.pk }}" class="button">
    {% trans 'hijack'|upper %}
</button>
```

And then in `HijackUserAdminMixin.response_action` you look for "hijack_user" in the POST and redirect the request to `views.AcquireUserView`. So again something like pseudocode:

```python

class HijackUserAdminMixin:
    # ...
    def response_action(self, request, queryset):
        if "hijack_user" in request.POST:
            return views.AcquireUserView.as_view()(request)
        return super().response_action(request, queryset)
```
You'd have to change `AcquireUserView.get_object` to look for the "user_pk" under a different value and you'd still have to pass in `request` in order to get `{% if request.user|can_hijack:another_user %}`, but at least it keeps it secure and doesn't add confusion for end-users (by making the request via GET, or allowing the ability to select multiple users for the hijack action)?
@codingjoe sorry, I totally forgot about that request. But I like what @kevinmarsh wrote.
I agree with @kevinmarsh this seems to be the best approach. It also allows us to get rid of the little hack we used to create the CSRF token. Actually, @kevinmarsh would you be interested in providing a patch? I am very busy right now working on Django features, I'd really appreciate the help especially, since you already dug into this topic.
I had a quick play around with it and [have a POC](https://github.com/django-hijack/django-hijack/compare/master...kevinmarsh:feature/remove-nested-form-issue-261) which I can try out in a real project later this week (hopefully) and if all looks good then I can flesh out the tests a bit more and create a PR. Can discuss in the PR how much of a breaking change it can/should be (if it makes it into the 3.0 rewrite release with the other breaking changes)
@kevinmarsh
I tried your branch in the real world. And it doesn't seem to work. This is what the requests look like. The hijack is not happening.

![image](https://user-images.githubusercontent.com/721963/119619749-f8eb7e80-be0c-11eb-95eb-32490d6672fc.png)
![image](https://user-images.githubusercontent.com/721963/119619807-09035e00-be0d-11eb-886e-983e15ae6e3c.png)

Moreover, in the debug it doesn't even go into this method for some reason.
```python
    def response_action(self, request, queryset):
        """
        Handle an admin action. This is called if a request is POSTed to the
        changelist; it returns an HttpResponse if the action was handled, and
        None otherwise.
        """
```

@codingjoe 
I've been thinking. Maybe we could redefine the basic template of the `change_list.html` page. Add necessary forms to the end of the page and add to the button `onclick` submit action for the desired form.
Hi @Mogost, hm… yes we should finish this up. It's the last thing blocking us from a proper release.
Regarding your idea, yes, but it seems risky, Django makes changes to Admin templates without warnings or backwards compatibility. I would probably just add a little JS script that performs the request, completely bypassing the template. It should also reduce the current complexity a little. Give me a minute. I'll work on a proposal right away, before I forget it again ;)

Resolving patch:
diff --git a/hijack/contrib/admin/admin.py b/hijack/contrib/admin/admin.py
--- a/hijack/contrib/admin/admin.py
+++ b/hijack/contrib/admin/admin.py
@@ -1,4 +1,4 @@
-from django.middleware.csrf import get_token
+from django import forms
 from django.shortcuts import resolve_url
 from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
@@ -12,6 +12,10 @@ class HijackUserAdminMixin:
     hijack_success_url = None
     """Return URL to which one will be forwarded to after hijacking another user."""
 
+    @property
+    def media(self):
+        return super().media + forms.Media(js=["hijack/hijack.js"])
+
     def get_hijack_user(self, obj):
         """
         Return the user based on the current object.
@@ -42,7 +46,6 @@ def hijack_button(self, request, obj):
             "hijack/contrib/admin/button.html",
             {
                 "request": request,
-                "csrf_token": get_token(request),
                 "another_user": user,
                 "username": str(user),
                 "is_user_admin": self.model == type(user),


Test patch:
diff --git a/hijack/tests/test_admin.py b/hijack/tests/test_admin.py
--- a/hijack/tests/test_admin.py
+++ b/hijack/tests/test_admin.py
@@ -11,9 +11,7 @@ def test_user_admin(self, admin_client):
         url = reverse("admin:test_app_customuser_changelist")
         response = admin_client.get(url)
         assert response.status_code == 200
-        assert (
-            b'<button type="submit" class="button">HIJACK</button>' in response.content
-        )
+        assert b"data-hijack-user" in response.content
 
     def test_related_user(self, admin_client, admin_user):
         url = reverse("admin:test_app_post_changelist")