David Todd
David Todd

Reputation: 63

How to logout after X minutes of inactivity on button click?

I am working on a simple project where I have a login system with multiple users where each user will login and save form entries in a file. I am trying to build session timeout so that after 2 minutes of inactivity it can log me out.

All above things works fine. Now I am trying to build this session timeout so that it can log me out after x minutes of inactivity and redirect me to login.php page.

Here is my index.php file:

<?php

declare(strict_types = 1);

// Start session.
session_start();

// Include helper functions.
require_once 'helpers.php';

// 2 mins in seconds
$inactive = 120; 

if(isset($_SESSION['timeout']) ) {
    $session_life = time() - $_SESSION['timeout'];
    if($session_life > $inactive)
    { 
        redirect('logout.php');
        return;
    }
}

$_SESSION['timeout'] = time();

// Redirect user to login page if not authenticated.
if (! check_auth()) {
    redirect('login.php');
    return;
}

?>
<!doctype html>
<html>
<head>
    <title>Home</title>
</head>
<body>
    <div>
        <h1>Website Title</h1> <a href="logout.php">Logout</a> </div>
    <div>
        <p>Welcome back, <?= $_SESSION['user_id'] ?>!</p>
    </div>
    <form method="post">
        <input type="text" name="field1" />
        <input type="text" name="field2" />
        <input type="submit" name="submit" value="Save Data"> </form>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script>
    $(function() {
        $('form').submit(function(e) {
            e.preventDefault();
            $.post({
                url: 'save.php',
                data: $(this).serialize(),
            }).done(response => {
                response = JSON.parse(response);
                if(response.message) {
                    alert(response.message);
                }
            });
        });
    });
    </script>
</body>
</html>

Here is my save.php file:

<?php
declare(strict_types=1);
// Start session.
session_start();
// Include helper functions.
require_once 'helpers.php';

// 2 mins in seconds
$inactive = 120; 

if(isset($_SESSION['timeout']) ) {
    $session_life = time() - $_SESSION['timeout'];
    if($session_life > $inactive)
    { 
        redirect('logout.php');
        return;
    }
}

$_SESSION['timeout'] = time();

// Redirect user to login page if not authenticated.
if (! check_auth()) {
    redirect('login.php');
    return;
}

// save form entries in a file

Here is my helpers.php file:

<?php
declare(strict_types=1);

if (! function_exists('check_auth')) {
    function check_auth(): bool
    {
        return isset($_SESSION['user_id']);
    }
}

if (! function_exists('logout'))
{
    function logout()
    {
        if (isset($_SESSION['user_id'])) {
            $trace = json_decode(file_get_contents('current_user.txt'));

            if ((int) $trace->user_id === $_SESSION['user_id']) {
                $content = isset($trace->revoked_user_id)
                ? json_encode(['user_id' => $trace->revoked_user_id, 'created_at' => (new DateTime('now'))->format('Y-m-d H:i:s')])
                : '';
                file_put_contents('current_user.txt', $content);
            }

            unset($_SESSION['user_id']);
            unset($_SESSION['timeout']);
        }
    }
}

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        header('Location: ' . $url, true, $status_code);
        die();
    }
}

Problem Statement

I am redirecting to logout.php file once it is idle for 2 minutes in index.php and save.php. Here is what I am noticing -

Below is the ajax call I have in my index.php page -

<script>
$(function() {
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if(response.message) {
                alert(response.message);
            }
        });
    });
});
</script>

I get a json parsing error for my point 2 whenever I click Save Data button after 2 minutes of inactivity. And value of response variable is full login.php in html. I am not sure why it is happening. Do we need to check for session validation in jquery itself before it calls save.php?

Upvotes: 1

Views: 697

Answers (1)

Will B.
Will B.

Reputation: 18416

In summary, your issue is caused by the redirect occurring in save.php when it is being requested by ajax.

What happens is the redirect() request is processed transparently and the results are being processed by the jQuery.ajax().done() closure, which is trying to call JSON.parse(response); for the HTML of login.php. You should be able to validate this by viewing the developer tools (usually F12) in your browser, when clicking the button.

The general approach to resolve the issue, is to determine if the request is a XMLHttpRequest and send a different response instead of redirecting.

Detect XMLHttpRequest

To determine if the request is coming from jQuery.ajax(), you would check the X-Requested-With request header.

isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'

JSON Response with redirect property

This approach returns a redirect property in the response and uses it to redirect from the jQuery.ajax().done() closure.

helpers.php

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
            //do not redirect AJAX requests
            echo json_encode(['redirect' => $url]);
        } else {
            header('Location: ' . $url, true, $status_code);
        }
        die();
    }
}

index.php

$(function() {
    "use strict";
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if (response.redirect) {
                //redirect user
                window.location.href = response.redirect; 
            }
            if (response.message) {
                alert(response.message);
            }
        });
    });
});

Status Code Response

This approach returns a status code other than 200 Ok, instead of redirecting and checks for the response status code in the jQuery.ajax() or statusCode: method(s).

helpers.php

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
            //do not redirect AJAX requests
            http_response_code(401); //Unauthorized - PHP 5.4+
            echo $url; //optionally output the url
        } else {
            header('Location: ' . $url, true, $status_code);
        }
        die();
    }
}

Here you can choose to handle the status code as desired. For the simplest approach, checking the fail() jqXhr object status code and perform a javascript redirect instead.

index.php

$(function() {
    "use strict";
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if (response.message) {
                alert(response.message);
            }
        }).fail(jqXhr => {
            if (jqXhr.status == 401) {
                //redirect to specified url in the response text
                window.location.href = jqXhr.responseText; 
               /* alternatively hard-code '/logout.php' */
            }
        });
    });
});

Upvotes: 2

Related Questions