Category: "PHP"

Session Timeout Recovery

It can be difficult for the client to know if the session has timed out on the server. This post describes the use of session storage to save and restore the data on a page input when a session times out.

Let's start with the timeout detection.

On the server side, this tiny piece of code checks if the user is logged in.

<?php
session_start();
header('HTTP/1.1 '.(!empty($_SESSION['logged_in']) ? '204 No content' : '403 Forbidden'));
exit;

The only response is either 204 No content which means the user is logged in, or 403 Forbidden, meaning the user isn't logged in or the session timed out.

On the client side, we'll start with the code that is "pinging" the server and saving the data:

"use strict";

// pingStatus is the session storage key which will contain either SAVED or NOT_SAVED to indicate whether there is session data saved.
var pingStatus = '_$ping_status$_';
var SAVED = 'saved';
var NOT_SAVED = 'not_saved';

// PREFIX is the session prefix key, intended to avoid namespace conflicts between other sessionStorage code
var PREFIX = '__$';

// The values of these types of inputs are not saved - you may want to save hidden inputs depending on how your page is coded
var DONT_SAVE = ["file", "button", "submit", "reset", "hidden"];

// The ping function 
function ping(key) {
    // key is used to identify the data - it can be any value you would like
    // A page name and id work well, that way, if there are multiple instances of the same page in the browser, conflicts should be avoided
    PREFIX = key;

    // pinger is the variable used to store the interval timer
    var pinger;

    // doPing pings the server
    function doPing() {
        var pReq = new XMLHttpRequest();
        pReq.addEventListener("load", function () {
            var i, items;

            // If the server doesn't respond with a 204, consider it a timeout
            if (this.status !== 204) {

                // Stop pinging
                clearInterval(pinger);

                // Allow the user to decide whether to save the data or abandon it
                if (confirm('Your session timed out.  Click OK save the page data or Cancel to discard any data that has not been sent to the server') === true) {
                    // Loop through all the inputs
                    items = document.querySelectorAll('input, select, textarea');
                    for (i = 0; i < items.length; i++) {
                        // Only save the items that aren't in the DONT_SAVE array
                        if (DONT_SAVE.indexOf(items[i].type) === -1 && items[i].id !== "") {

                            // If it isn't a checkbox or radio button - save the value
                            if (items[i].type !== "checkbox" && items[i].type !== "radio") {
                                sessionStorage.setItem(PREFIX + items[i].id, items[i].value);
                            } else {
                                // If it is a checkbox or radio button - save whether it was checked or not
                                sessionStorage.setItem(PREFIX + items[i].id, items[i].checked);
                            }
                        }
                    }
                    // Set the flag to indicate the data was saved
                    sessionStorage.setItem(pingStatus + key, SAVED);
                } else {
                    // Set the flag to indicate the data has not been saved
                    sessionStorage.setItem(pingStatus + key, NOT_SAVED);
                }

                // Location reload is assuming that when the page reloads, the server will redirect to the login page, then to the requested page - which is the current page
                location.reload();
            }
        });

        // Issue the ping request
        pReq.open("GET", "ping.php");
        pReq.setRequestHeader("Accept", "application/json");
        pReq.send();
    }
  
    // Set the flag to indicate the data has not been saved
    sessionStorage.setItem(pingStatus + key, NOT_SAVED);

    // Set up to ping every 10 seconds
    pinger = setInterval(doPing, 10000);

    // Do a first ping, which is effectively on page load.  This catches timeouts when the computer had gone to sleep
    doPing();
}

The restore code checks to see if the data has been saved and if there is data, allows the user to use it or discard it.


function restore(key) {
    var v, i, items;

    PREFIX = key;

    // If there is data saved for this session
    if (sessionStorage.getItem(pingStatus + key) === SAVED) {

        // Give the user the opportunity to use it or ignore it
        if (confirm('There is data saved for this page, click OK to load it or Cancel to discard it') === true) {

            // Get all the inputs
            items = document.querySelectorAll('input, select, textarea');
            for (i = 0; i < items.length; i++) {

                // Ignore the 'DONT_SAVE's and anything that doesn't have an id
                if (DONT_SAVE.indexOf(items[i].type) === -1 && items[i].id !== "") {
                    v = sessionStorage.getItem(PREFIX + items[i].id);

                    // If there is no data saved for the input, it can't be restored
                    if (v !== null) {

                        // If it's not a checkbox or radio button, restore the value
                        if (items[i].type !== "checkbox" && items[i].type !== "radio") {

                            // If the value is different than what loaded, restore the value
                            if (items[i].value !== v) {
                                items[i].value = v;
                            } 
                        } else {
                            // If it is a checkbox or radio button, set the checked state
                            items[i].checked = v;
                        }
                    }
                }
            }
        }
        // Indicate the data has not been saved
        sessionStorage.setItem(pingStatus + key, NOT_SAVED);
    }
}

Finally, to integrate it into the page, include the ping.js file in a script tag, create and save a key, then call restore(key) followed by ping(key).

var key = "page"+someId;restore(key);ping(key);

Post courtesy of Game Creek Video

Variable number of parameters on a prepared statement in PHP

Sometimes, you need to use a different number of parameters for a prepared statement. The call_user_func_array function allows you to call bind_param with a varying number of parameters.



// Start by validating your data
$something = filter_input(INPUT_POST, 'something', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^(this|that|other|thing)$/i']]);

// Set up the types string
$types = '';

// Initialize the array the variables will be referenced from
$values = [];

// Create the base SQL statement
$query = 'SELECT * FROM table';

// Initialize an array to store the WHERE comparisons
$where = [];

// Check to see if $something should be used 
// empty tests for both null (no data in input) and false (invalid data)
if (!empty($something)) {
   
    // Set the WHERE comparison for something
    $where[] = 'something = ?';

    // Append the type for this comparison
    $types .= 's';

    // Add a reference to the $something variable (that's what the ampersand is)
    $values[] = &$something;

}

// If the $where array has elements, add them to the query
if (count($where) > 0) {
    $query .= ' WHERE '.implode(' AND ',$where);
}

$stmt = $mysqli->prepare($query);

// Create the array of parameters which will be sent to the bind_param method
$params = array_merge([$types],$values);

// Bind the variables
call_user_func_array([$stmt,'bind_param'],$params);

What Branch am I on Anyway?

Often during web development the code is switching between different branches and it can be difficult to let everyone on a small team know which branch is in use.

Adding this line to a PHP script and using CSS to make it easy to see can save time.

<div class="git-branch">'.`git name-rev --name-only HEAD`.'</div>

Be sure to wrap it in some code to prevent display on the production site.

One Approach to Complying with a "script-src 'self'" Content Security Policy

I recently encountered this error when working with plugin code on an application:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' chrome-extension-resource:".

The cause of the error was inline script code I was using to pass values from the server to the client.

After a bit of research (see the link above), the best solution looked like a little bit of PHP code to create the JavaScript required to pass the values to the client.

The overhead of checking the timestamp and creating the file is minimal, so this code recreates the JavaScript once each day.

<?php

Class CSP {
	const JSFILENAME = 'csp.js';

	static public function cspFilename($dir = __DIR__) {
		return $dir.'/'.self::JSFILENAME;
	}

	static public function cspFileNeedsRebuild($filename) {
		if (!is_file($filename)) {
			return true;
		}
		$fileLastModified = date('z',filemtime($filename));
		$today = date('z');
		return $fileLastModified !== $today;
	}
}

$someValue = 'Some value';
$jsFilename = CSP::cspFilename();
if (CSP::cspFileNeedsRebuild($jsFilename)) {
	$js = 'var someValue = "'.$someValue.'";'.PHP_EOL;
	file_put_contents($jsFilename,$js);
}
echo '<script src="'.$jsFilename.'"></script>'; 

Other solutions I could have used would have been to disable the Content Security Policy, but that's really a stupid approach. There is also nonce and one may code the policy with more complex values.

Twitter Application Auth Sample - PHP

This is a sample PHP code which can be used to get a Twitter OAuth token for use in making API calls.

It includes a trends available request that gets the list of countries for which Twitter trends are available.

Be sure to read the documentation at the link above. A given application can only have one token at any given time, so once established, the token should be stored and reused.


        $consumerKey = '-- YOUR CONSUMER KEY --';
        $consumerSecret = '-- YOUR CONSUMER SECRET --';
        $encodedKey = urlencode($consumerKey);
        $encodedSecret = urlencode($consumerSecret);
        $bearerTokenCredentials = $encodedKey.':'.$encodedSecret;
       
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/oauth2/token');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
        curl_setopt($ch, CURLOPT_HTTPHEADER,
                array('Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
                        'Authorization: Basic '.$base64BearerTokenCredentials));
        $result = curl_exec($ch);
        $error = curl_errno($ch);
        if ($error === 0) {
                $json = json_decode($result);
                curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/1.1/trends/available.json');
                curl_setopt($ch, CURLOPT_HTTPGET, true);
                curl_setopt($ch, CURLOPT_POST, false);
                curl_setopt($ch, CURLOPT_HTTPHEADER,
                                array('Authorization: Bearer '.$json->access_token));
                $result = curl_exec($ch);
                $error = curl_errno($ch);
                curl_close($ch);
                if ($error === 0) {
                        $json = json_decode($result);
                        $countries = array();
                        foreach ($json as $location) {
                                if ($location->placeType->name == 'Country') {
                                        $countries[$location->woeid] = $location->name;
                                }
                        }
                        asort($countries);
                }
        }