Category: "JavaScript"
DataTables - Passing Data to the Server in Client-Side processing mode
Oct 26th
DataTables is AWESOME. I use it for list-based selection, table-based editing, data display and filtering and anything else I can think of because it is so robust that virtually anything is possible. Really.
If the dataset being used is fairly small (you may interpret small any way you like), DataTables client-side processing is amazing. It sorts and filters, updates all the navigation, lets you adjust the number of records displayed - as well as many more optional features - with very little effort.
Client-side processing - where filtering, paging and sorting calculations are all performed in the web-browser.
Ref: https://datatables.net/manual/data/#Client-side-processing
but
For client-side processing no additional data is submitted to the server
Ref: https://datatables.net/reference/option/ajax.data
The goal was to have a single client-side datatable which would display different content based on the user's actions. For example, 'list all the items with a status of "new" and a (time) segment of 1'.
HTML for the DataTable
<table id="datatable" class="display" cellspacing="0" width="100%">
<thead>
<tr>
<th>Id</th>
<th>Vendor</th>
<th>Item</th>
<th>Date</th>
</tr>
</thead>
</table>
JavaScript
var datatable;
$(function($){
datatable = $('#datatable').DataTable(
{
"ajax": {
"url": "data-10-1.php"
},
"columns":
[
{"data": "link"},
{"data": "vendor"},
{"data": "item"},
{"data": "date"}
]
});
// Handle the click events
$(".summary").on("click", function(evt){
var target = evt.target, status, segment;
if (target.hasAttribute("data-status")) {
status=$(target).attr("data-status");
segment=$(target).attr("data-segment");
datatable.ajax.url("data-"+status+"-"+segment+".php");
datatable.ajax.reload();
datatable.draw('full-reset');
}
});
});
Notice that the URLs are variable. The status and segment values are passed in the URL itself, there is no POST or GET data.
Sample HTML which invokes the event handler. This HTML is inside the div with class="summary".
<span class="details" data-status="10" data-segment="4" data-status-text="new">84</span>
In order to extract the status and segment out of the URL, you can use Apache RewriteRules, like so:
<directory /var/www/html>
RewriteEngine On
RewriteRule ^(data)-(\d+)-(\d+)(\.php)$ $1$4?status=$2&segment=$3 [L]
</directory>
status and segment can then be accessed as $_GET variables. Be sure to validate them.
How does it work? As the user clicks on the spans, the event handler reads the data attributes and constructs a URL that is sent to the server. When Apache receives the request, it rewrites it to deliver the data embedded in the URL as GET parameters.
This post courtesy of Game Creek Video
Web application session timeout code
Aug 28th
Session timeout warning for a web application with a variety of page layouts and frequent use of multiple tabs.
Nutshell explanation - ping the server, if you get a 403, show a huge red bar across the top of whatever page pinged.
Result:
- It is immediately apparent the tab has timed out
- If one tab times out, it does not disrupt the others
- There is a link to help the user log in again
function sessionPing() {
var pinger;
function doPing() {
var pReq = new XMLHttpRequest();
pReq.addEventListener("load", function () {
var sessionExpired, sessionExpiredMessage;
var reloadLink;
if (this.status === 403) {
clearInterval(pinger);
sessionExpired = document.createElement("div");
// Sometimes an inline style is really the best solution
sessionExpired.setAttribute("style","display:block; width: 100%; line-height: 2.5em; position:absolute; top:0; z-index:10000; text-align: center; background-color: #f00; color: #fff; font-family: 'Trebuchet MS',sans; font-size: 1.5em; font-style: italic");
sessionExpiredMessage = document.createTextNode("Session expired ");
sessionExpired.appendChild(sessionExpiredMessage);
reloadLink = document.createElement("a");
reloadLink.href = location.href;
reloadLink.textContent = "Click to Continue";
reloadLink.setAttribute("style","font-size:0.7em;color:#ddd");
sessionExpired.appendChild(reloadLink);
document.body.insertBefore(sessionExpired, document.body.firstChild);
document.title = "Session expired";
}
});
pReq.open("GET", "/ping.php");
pReq.send();
}
pinger = setInterval(doPing, 30000);
}
sessionPing();
<?php
session_start();
if (empty($_SESSION['user_id'])) {
header('HTTP/1.1 403 Forbidden');
exit;
}
header('HTTP/1.1 201 No content');
exit;
One may argue that the page should be cleared, in this case, I chose to leave it up so people can copy the content off.
Confession: I didn't test this code, it is an extract.
This post courtesy of Game Creek Video
Session Timeout Recovery
Jul 24th
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);
One Approach to Complying with a "script-src 'self'" Content Security Policy
Mar 23rd
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.
A Very Simple Node/Express JSON Form Example
Apr 19th
Server-side (file is named server)
This code is invoked with: node server
Once it is invokec, call the page with http://example.com:1337/form.html
// Express is a framework used with node that provides many important constructs
var express = require('express');
// The application is an Express application
var app = express();
// bodyParser is used to convert the request data into JSON
var bodyParser = require('body-parser');
app.use(bodyParser.json());
// Express.static is used to serve the static files
app.use(express.static('static'));
// This receives a request posted to / and processes it
app.post('/', function (req, res) {
var data = req.body;
if (data['first-name'].trim().length > 5 && data['last-name'].trim().length > 5) {
res.end(JSON.stringify({"status":"success"}));
} else {
res.end(JSON.stringify({"status":"error"}));
}
});
// The application will listen on port 1337
app.listen(1337);
Client-side
The client side code is in the static directory. There is a simple form:
<form>
<label for="first-name">First name<input type="text" id="first-name"></label>
<label for="last-name">Last name<input type="text" id="last-name"></label>
<button type="button" id="go">Go</button>
</form>
<div id="status">
</div>
And a little bit of JavaScript.
This code fires when a button with the id of "go" is clicked. It gets the values of the inputs, creates an object and sends it to the server.
document.getElementById("go").onclick = function() {
var i,v,inp = ["first-name","last-name"];
var data = {};
for (i in inp) {
v = inp[i];
data[v] = document.getElementById(v).value;
}
var xmlhttp;
xmlhttp=new XMLHttpRequest();
xmlhttp.addEventListener("load", done, false);
function done()
{
response = JSON.parse(xmlhttp.response);
document.getElementById("status").textContent = response["status"];
}
xmlhttp.open("POST","http://example.com:1337",true);
xmlhttp.setRequestHeader("Content-type","application/json");
xmlhttp.setRequestHeader("Accept","application/json");
xmlhttp.send(JSON.stringify(data));
}
Notes for those who are patient or really desperate.
- app.post doesn't make a post request, it accepts one with the URL in the first parameter. In this case, it will accept a post response for / on port 1337.
- Express is a nice framework for node. Use it.
- You need to use npm to install both Express and body-parser.
- bodyParser parses the request body from JSON. It can handle other formats, too
- Be sure to put the static files in the static directory, or update the express.static call accordingly.
- Note that there is no charset on the content type header sent to node. Node is looking for "application/json"
- The validation is really limited, the goal of this code is to show how to set up the form.
- You may have to update your firewall settings to allow access to port 1337. If you are on an Amazon instance, use their web console.
- When all else fails, read the documentation.