Remember requested path in session via controller

I’m trying to create a private website, where visitors need to have a login to be able to see the content. I want to reuse the Kirby Panel login page for that.

I’ve created a default controller site/controllers/site.php, where I want to check whether the user is logged in or not. If he is not, I want to redirect him to the login page. But after a successful login I want to redirect him back to the initially requested page (this functionality is alread implemented by default in src/Panel/Home.php). So I need to set the panel.path property in the session.

<?php

return function ($kirby) {
  $error = false;

  if (!$kirby->user())
  {
    $intendedPath = $kirby->route()->arguments()[0] ?? null;
    $kirby->session()->set('panel.path', $intendedPath);
    go('panel');
  }

  return [
    'error' => $error,
  ];
};

However, the default controller is called on EVERY browser request. So in my session the panel.path value is always being set to favicon.ico, because apparently this is the last request coming from my browser.

Any idea how to “filter” the requests, so I only write to the session, when the request was for a page, and not for single files?

At the risk of stating the obvious: wouldn’t it be possible to check with an if clause whether the returned path is a page (e.g. checking for file names in the path with a \.(.*{3,4})$ pattern)?

That said, I had difficulties replicating your issue. The above controller only triggers once for the page in my test setup. Interestingly, for some pages it does not trigger at all; need to figure out what that’s about.

$kirby->route()->arguments()[0] led to a “Undefined offset” error on the home page, so maybe $kirby->request()->path()->toString() could be an alternative there?

PS: Closely following your work on this, @pReya – impressive PRs on the Panel bugs; looking forward to get this use case working :partying_face:

Yes, a regex would probably be possible, but I’d like to see if there are any other/more elegant solutions before I try this.

Does your test setup use a favicon? What webserver? Maybe your server config handles this differently. I’m working on a local Laravel Valet setup without a favicon.

The undefined offset error for the home page is weird. There is a null coalescing operator there, so it should always use null. It works for me.

I guess a related question from this experience is: Is the global/default site controller really supposed to be called on every request, even if the request is for files? Shouldn’t it automatically, by default only run for “page” requests?

Happy to know that somebody might want to use this stuff, too. Will create a cookbook article once this is all working as expected.

That’s what I thought, hence the “stating the obvious” disclaimer…

Valet Linux on Nginx, with Favicon set up. Maybe the favicon.ico request only occurs if no favicon is configured? So the fact that the global site controller (something I haven’t really worked with before, so my experience is limited) is triggered might have to do with a fallback for non-existing file URIs?

You are of course right; sorry 'bout that – I had modified the code to run some alternative tests and the null coalescing operator got lost. User error :slight_smile:

I did some more experiments (and read up properly on global controllers) and I believe the reason you are seeing the Favicon path is because the error page is triggered (which is then dealt with via the router).

Here are some potential ideas for a more elegant solution than a regex:

  • $kirby->response()->code() returns 200 for valid requests; would solve the Favicon issue (I tried; it returns 404 when an error page is called, as can be expected), but might still return 200 for some odd URLs?
  • $page->intendedTemplate()->name() returns special for the error page in my setup; checking for that could allow to rule out or only allow certain templates
1 Like

Thank you very much for giving me a lot of good ideas.

Your suggestion about the error page is partially correct. However it is actually the other way round: The favicon is being requested first, the default controller is being called, and then the error page is returned.

So I would still be interested in anyone from the team commenting on this: Is it intended, that the default controller is being called on every request (including files like favicon)?

Are you absolutely sure that $kirby->response()->code() returns 200 for you? Because I have tested it, and it seems there is a bug in it. For me it never returns 200 (404 works correctly though). I’ve created a new bug ticket for this: response()->code() does not return 200 as expected · Issue #4082 · getkirby/kirby · GitHub

$page->intendedTemplate()->name() is a good idea. Thank you for this idea, I did not know about this. Will try to make it work with this function for now.

This seems weird. The controller should only be called on pages and only if there is no specific page controller and I can’t find evidence in the code that it might be called anywhere else.

If that should indeed be the case, that would be a bug.

How did you test when the controller is called?

Seems I was wrong about this. It is indeed the error page that calls the default controller. Sorry, my mistake.

I am absolutely sure I simply deduced it has to be 200 for an existing page as I got a 404 for a non-existing URL :see_no_evil: So you are right, I do not get a 200 here, sorry for the confusion.

I commented on the Github ticket, as I believe this is actually correct behaviour as Kirby has likely not assembled a Response object by the time a default controller is invoked for an existing page.

Sorry @texnixe (or @sebastiangreger), I know this is slightly off-topic, but also somehow connected to the remembered path in the session. I have to continue with this favicon topic for a little bit :smiley: I thought I understood the logic, but it turns out I don’t. Can you explain the following behavior to me?

  1. New Plainkit project
  2. Create a new content page via Panel (it will use template default)
  3. Create a new controller in site/controllers/default.php (not to be confused with site.php – this is a regular per-template controller we’re creating) and add some output via echo, e.g.
<?php
return function () {
  echo("DEFAULT CONTROLLER");
  return true;
};
  1. Open your newly created content page → You should see “DEFAULT CONTROLLER” in your browser, as expected
  2. Open mydomain.com/favicon.ico in your browser (it doesn’t exist) → You will see the error page, but you will also see “DEFAULT CONTROLLER” above your content. Why is the default controller even being called? The error page does not use the default template.

Unless you created a special template for the error page, it will use the default controller (as will any other page that doesn’t have a specific template). So what happens is the expected behavior.

So the default controller/template is special? I thought there was only one special case of controller/template name, which is the site.php controller.

So the order of precedence for controllers would be:

  1. Is there an exact match for the template name? E.g. myPage
  2. If no: Is there a default?
  3. If no: Is there a site?

EDIT: It gets clearer looking at the documentation for templates. Unfortunately the docs for controllers are a little misleading. Under the headline “Default controller” you will find the site controller, but the default controller is never mentioned.

The default.php controller is only used for the default.php template. The connection between content file, template and controller is always by filename and the default.php template is just a fallback.

The only exception from the naming scheme is the site.php controller. If it exists and there is no specific controller for a template, the site.php controller is used.

2 Likes

Yes, the naming there is a bit unfortunate. Maybe we should call it “general controller” to avoid confusion?

1 Like

So, after a lot of confusion, I’m back to my original plan (using the Kirby login page as a login for Frontend users – users who have no panel access), including redirecting them back to their original requested location after the login. I’ve stopped using both the default and or the “site” controller for doing this. Instead, here is my “protected” controller:

<?php

use Kirby\Cms\Url;

return function ($kirby) {
    $error = false;

    if (!$kirby->user()) {
        $intendedPath = $kirby->route()->arguments()[0] ?? null;

        if (isset($intendedPath)) {
            // This needs to be an absolute URL, a relative path to a frontend
            // page will not work (b/c the panel will always add '/panel/' to
            // the path)
            $intendedPath = Url::to($intendedPath);
            $kirby->session()->set('panel.path', $intendedPath);
            $error = true;
            go('panel/login');
        }
    }

    return [
        'error' => $error,
    ];
};

However for some reason, the panel.path in the Session will always be overridden by another value. I’ve found out, that somehow the login.fallback route in kirby/config/areas/login.php will always mess up my path in the session. If I uncomment this route, everything works like a charm.

<?php

use Kirby\Panel\Panel;

return function ($kirby) {
    return [
        'icon'  => 'user',
        'label' => t('login'),
        'views' => [
            'login' => [
                'pattern' => 'login',
                'auth'    => false,
                'action'  => function () use ($kirby) {
                    $system = $kirby->system();
                    $status = $kirby->auth()->status();
                    return [
                        'component' => 'k-login-view',
                        'props'     => [
                            'methods' => array_keys($system->loginMethods()),
                            'pending' => [
                                'email'     => $status->email(),
                                'challenge' => $status->challenge()
                            ]
                        ],
                    ];
                }
            ],
            // Uncommenting this block makes my controller work
            // 'login.fallback' => [
            //     'pattern' => '(:all)',
            //     'auth'    => false,
            //     'action'  => function ($path) use ($kirby) {
            //         /**
            //          * Store the current path in the session
            //          * Once the user is logged in, the path will
            //          * be used to redirect to that view again
            //          */
            //         $kirby->session()->set('panel.path', $path);
            //         Panel::go('login');
            //     }
            // ]
        ]
    ];
};

Any ideas what happens here? Can anyone explain to me, what this login.fallback route is doing?

When a user logs in to the Panel, they normally get redirected to the last view they were on before they were logged out,

I’d say the login.fallback is responsible for that.

@distantnative Do you maybe have any idea, how to allow frontend pages as redirect targets in the panel.path session value, so it doesn’t collide with the login.fallback route?

I just gave this a try on the develop branch with the upcoming changes for 3.6.2 and it worked flawlessly. Here’s what I did; maybe this helps tracking down where your approach runs into that error?

1 – Created a route for login/(:all), but that’s of course just one of many ways to set the logintarget in the session:

// in config.php -> routes
[
    'pattern' => 'login/(:all)',
    'action' => function ($path = null) {
        kirby()->session()->set('myplugin.logintarget', $path);
        go('panel/login');
    }
],

2 – Created a user blueprint for the role I am targeting (for testing purposes, I simply used admin.yml temporarily):

# admin.yml
home: "{{ site.logintarget }}"
permissions:
  access:
    panel: false

3 – Created a plugin providing the site method logintarget:

// site/plugins/myplugin/index.php
Kirby::plugin('myplugin/logintarget', [
    'siteMethods' => [
        'logintarget' => function () {
            $target = kirby()->session()->pull('myplugin.logintarget');
            if ($target === null) {
                return '/panel';
            }
            return $target;
        }
    ]
]);

Now when I go to https://example.com/login/blog, I get forwarded to the login form and after entering my credentials end up on the https://example.com/blog page, now logged in as a user. If I go directly to the login form without a pre-existing session, I get forwarded to https://example.com as I’d expect (since access to the panel is forbidden and no logintarget stored in the session).

This is where I originally ended up with the two bugs that have now been fixed. What context am I missing to reproduce the error you described?

@sebastiangreger Wow! Thanks so much for this input! It is a different approach to mine. I tried to “reuse” the existing panel.path in the session, which Kirby automatically uses to redirect to.

But now that I see your approach, using your own session value via plugin in combination with the (now fixed) home: property of the user blueprint makes much more sense.

I will implement this over the weekend, and see if I still discover any problems with it. But this is looking extremely good. Thanks so much for your input!

1 Like

:+1:

Please note I edited the plugin code in my example above after some more experimentation – we have to check for the result of the session pull. While home: null in a user blueprint works just fine to send the user to the panel, it does for some reason not work when the page method returns null …I kept ending up on the frontend root URL instead.

…only applies to users with panel access, of course. Otherwise the default behaviour is probably fine.

1 Like