Turtle Doodle presents several panels which arrange themselves responsively. There is a canvas where the Turtle will move, leaving a paint trail behind him. A control panel allows various commands to be sent to the Turtle such as Forward, Back, Left, Right. An "info" area shows a textual run-length-encoded listing of all the Turtle commands that have been executed. Attached to the "info" area is a listing of the "undo" stack showing what pieces will be added when redo is clicked. Finally, iff the session is logged in to the database (using the "sign in" or "sign up" buttons) another panel lists all the saved doodles which can be loaded by clicking.
This is my first project using php, and the largest chunk of javascript I've written so far. I'd like to know how I might better organize all the javascript code. I think it isn't horrible at the moment, but I feel that it's right on the verge of becoming unmanageble if I start adding more features.
Sizes (and # description
) of all my files:
$ shopt -s extglob
$ wc *([^.]).{php,js,css}
66 105 1521 index.php # entry point
110 327 3684 page.php # page html
29 36 252 style.css # style tweaks
690 2043 15729 script.js # page js
177 542 4280 db.php # database functions
4 3 57 create.php # script - create db tables
4 3 52 delete.php # script - delete db tables
4 3 55 list.php # script - list all tables
7 15 133 load.php # script - simulate ajax request
1091 3077 25763 total
The directory also has local copies of jQuery and w3.css. I have the local jQuery arranged as a backup to the cdn link, but I haven't quite managed to arrange the same magic for w3.css yet. (see the top of page.php
for this part)
index.php
This is the entry point for the project. In response to GET
requests, it produces the html content from page.php
. In response to POST
requests, it checks the $_POST['req']
field for a command name and then calls the appropriate database function defined in db.php
.
<?php
session_start();
if( $_SERVER['REQUEST_METHOD'] == 'POST' ){
handle_post();
} else {
require "page.php";
}
function set_session_variables(){
if( isset($_SESSION['user']) ){
?>
<script>
sessionStorage.setItem('user', '<?php echo $_SESSION['user']; ?>' );
sessionStorage.setItem('pass', '<?php echo $_SESSION['pass']; ?>' );
</script>
<?php
} else {
?>
<script>
sessionStorage.removeItem('user');
sessionStorage.removeItem('pass');
</script>
<?php
}
}
function handle_post(){
global $pdo;
include("db.php");
switch( $_POST['req'] ){
case 'signin':
sign_in($_POST['user'], $_POST['pass']);
header("Location: ");
break;
case 'signup':
sign_up($_POST['user'], $_POST['mail'], $_POST['pass']);
header("Location: ");
break;
case 'signout':
sign_out();
session_unset();
$_SESSION = array();
session_destroy();
header("Location: ");
break;
case 'save':
save_doodle($_POST['user'], $_POST['pass'],
$_POST['name'], $_POST['drawing']);
break;
case 'delete':
delete_doodle($_POST['user'], $_POST['pass'],
$_POST['doodle']);
break;
case 'load':
header('Content-Type: application/json');
load_doodles($_POST['user'], $_POST['pass']);
break;
}
$pdo = null;
}
page.php
This file produces the html content for the page. If the session is logged in, it includes a bit of javascript (defined in index.php
) to set the user
and pass
variables in sessionStorage
. I've adopted a style here which deliberately omits any markup which isn't absolutely necessary. So, I never close <p>
tags. And there isn't even a <body>
tag. This seems to really help in making less code overall.
<!doctype html>
<html lang=en>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset=utf-8>
<title>Turtle Doodle</title>
<link href=w3css.4.w3.css rel=stylesheet type=text/css>
<!-- https://www.w3schools.com/w3css/4/w3.css -->
<link href=style.css rel=stylesheet type=text/css>
<script src=https://code.jquery.com/jquery-3.4.1.min.js></script>
<script>window.jQuery ||
document.write('<script src="jquery.3.4.1.min.js"\x3C/script>');</script>
<?php set_session_variables(); ?>
<script src="script.js"></script>
<header class="w3-container w3-green" >
<div id=brand class="w3-container w3-half">
<h1 ><nobr >Turtle Doodle</nobr></h1>
</div>
<div class="sign w3-container w3-half w3-bar w3-lightgreen" >
<div class="signedout w3-lightgreen" >
<button class="w3-bar-item w3-button" id="signin" >Sign In</button>
<button class="w3-bar-item w3-button" id="signup" >Sign Up</button>
</div>
<div class="signin w3-lightgreen" style="display:none" >
<form method=post >
<input type=hidden name=req value=signin >
<input class="w3-bar-item" type=text size=10
name=user id=userin placeholder="Username">
<input class="w3-bar-item" type=text size=10
name=pass placeholder="Password">
<button class="w3-bar-item w3-button" type=submit >Sign In</button>
</form>
</div>
<div class="signup w3-lightgreen" style="display:none" >
<form method=post >
<input type=hidden name=req value=signup>
<input class="w3-bar-item" type=text size=10
name=user id=userup placeholder="Username">
<input class="w3-bar-item" type=text size=10
name=mail placeholder="Email">
<input class="w3-bar-item" type=text size=10
name=pass placeholder="Password">
<button class="w3-bar-item w3-button" type=submit >Sign Up</button>
</form>
</div>
<div class="signout w3-lightgreen" style="display:none" >
<form method=post >
<input type=hidden name=req value=signout>
<button class="w3-bar-item w3-button" >Signout</button>
</form>
</div>
</div>
</header>
<main class="w3-container w3-row" >
<section class="canvas w3-container w3-col l6 m9 s12" >
<canvas>
</section>
<section class="controls w3-container w3-col l3 m3 s6" >
<h2 >controls</h2>
<nobr >
<button id=moveForward >F ↑</button>
<button id=Slink >S</button>
<input id=step type=text size=1 value=20 onFocus=this.select()>
<button id=moveBack >B ↓</button>
</nobr>
<nobr >
<button id=turnLeft >L ↶</button>
<button id=Alink >A</button>
<input id=angle type=text size=1 value=90 onFocus=this.select()>
<button id=turnRight >R ↷</button>
</nobr>
<p>
<nobr >
<button id=Tlink style="display:none" >Turtle</button>
<button id=Nlink >No Turtle</button>
</nobr>
<p>
</section>
<section class="info w3-container w3-col l3 m3 s6" >
<input id=doodleName type=text size=10 onFocus=this.select()>
<button align=right id=save >Save</button>
<button align=right id=new >New</button>
<h2 ><span align=left>info</span>
<span align=right>
<button id=undo align=right >Undo</button>
</span>
</h2>
<p><div class=doodle ></div></p>
<h2 align=right >
<button id=redo >Redo</button>
</h2>
<p align=right><div class=undo align=right></div></p>
</section>
<section class="saved w3-container w3-col l6 m12 s12" >
<h2 >doodles</h2>
<ol class=list >
</ol>
</section>
</main>
<footer class="w3-container" >
<p >Inspired by the ideas of Seymour Papert
<br >© M. Joshua Ryan 2019
<br ><span id=debug ></span>
</footer>
style.css
I'm using w3.css to handle responsive layout and basic styling. So this file just defines a few colors and tweaks.
h1 {
font: 2em/1.5 sans-serif;
}
#sign {
padding: .5em;
}
ul li {
display: inline;
padding: 0 1em;
}
main {
font: 11px/1.2 serif;
}
h2 {
font: 1.5em/1.2 sans-serif;
background: lightgreen;
color: darkgreen;
}
script.js
The big pile of javascript. I wonder if there are opportunities to make better use of jQuery constructs to simplify the js? Roughly, the top few functions are involved in attaching event handlers; the next portion implements these handlers and calling appropriate model-transforming functions; the next portion is a grab bag of all the remaining functions.
Would any of my (sets of) functions abstract nicely into separate modules?
var List = [
];
var Doodle = {
drawing: [],
undo: []
};
$(function(){
let cx = $('canvas')[0].getContext('2d');
test_stroke( cx );
attach_controls();
load_local_doodle();
set_default_values();
if( sessionStorage.getItem('user') ){
show_signout();
load_doodles();
}
update();
});
function test_stroke( cx ){
cx.strokeStyle='blue';
cx.lineWidth=2;
cx.moveTo( 10, 10 );
cx.lineTo( 20, 20 );
cx.stroke();
}
function attach_controls(){
$("#signin").click(show_signin);
$("#signup").click(show_signup);
keys_on();
$("button.signout").click(destroy_session);
$("#moveForward").click(moveForward);
$("#moveBack").click(moveBack);
$("#turnLeft").click(turnLeft);
$("#turnRight").click(turnRight);
$("#Slink").click(focusStep);
$("#step").keypress(numeric);
$("#step").change(setStep);
$("#Alink").click(focusAngle);
$("#angle").keypress(numeric);
$("#angle").change(setAngle);
$("#Tlink").click(showTurtle);
$("#Nlink").click(hideTurtle);
$("#save").click(save);
$("#undo").click(undo);
$("#redo").click(redo);
$("#new").click(new_doodle);
$("#doodleName").focus(()=>keys_off());
$("#doodleName").change(()=>{Doodle.name = $("#doodleName").val();keys_on()});
}
function keys_on(){
document.addEventListener("keydown",onKey);
//$("document").keypress(onKey);
}
function keys_off(){
document.removeEventListener("keydown",onKey);
//$("body").off("keypress");
}
function set_default_values(){
let canvas = $('canvas')[0];
fix_canvas();
window.matchMedia( "(min-width: 400px)" ).
addEventListener( "change", fix_canvas);
window.matchMedia( "(min-width: 600px)" ).
addEventListener( "change", fix_canvas);
$("#step").val(20);
$("#angle").val(90);
}
function fix_canvas(){
if( window.matchMedia( "(min-width: 601px)" ).matches ){
fix_canvas_large();
} else if( window.matchMedia( "(min-width: 993px)" ).matches ){
fix_canvas_mid();
} else {
fix_canvas_small();
}
update();
}
function fix_canvas_small(){
let canvas = $('canvas')[0];
if( is_portrait() ){
canvas.width = -30 + $(window).width() * 12/12;
canvas.height = $(window).height() * 2/5;
} else {
canvas.width = -30 + $(window).width() * 12/12;
canvas.height = $(window).height() * 2/5;
}
}
function fix_canvas_mid(){
let canvas = $('canvas')[0];
if( is_portrait() ){
canvas.width = -30 + $(window).width() * 9/12;
canvas.height = $(window).height() * 2/5;
} else {
canvas.width = -30 + $(window).width() * 9/12;
canvas.height = $(window).height() * 2/5;
}
}
function fix_canvas_large(){
let canvas = $('canvas')[0];
if( is_portrait() ){
canvas.width = -30 + $(window).width() * 6/12;
canvas.height = $(window).height() * 3/5;
} else {
canvas.width = -30 + $(window).width() * 6/12;
canvas.height = $(window).height() * 3/5;
}
}
function is_portrait(){
return ($(window).height / $(window).width) > 1;
}
function onKey( event ){
//debug( "key: " + event.which );
if( event.ctrlKey ){
return true;
}
switch( event.which ){
case A('f'): case A('F'): case 38: return moveForward();
case A('l'): case A('L'): case 37: return turnLeft();
case A('r'): case A('R'): case 39: return turnRight();
case A('b'): case A('B'): case 40: return moveBack();
case A('a'): case A('A'): return focusAngle();
case A('s'): case A('S'): return focusStep();
case A('t'): case A('T'): return showTurtle();
case A('n'): case A('N'): return hideTurtle();
}
}
function A( c ){ return c.charCodeAt(0); }
function numeric( event ){
let whence = event.target;
let c = event.keyCode || event.which;
if( A('0') <= c && c <= A('9') ){
return true;
} else {
event.preventDefault();
event.stopPropagation();
whence.blur();
//doodle_add_mod( whence );
onKey( event );
return false;
}
}
function show_signin(){
$(".signedout").hide();
$(".signin").show();
$("#userin").focus();
keys_off();
return false;
}
function show_signup(){
$(".signedout").hide();
$(".signup").show();
$("#userup").focus();
keys_off();
return false;
}
function show_signout(){
$(".signedout").hide();
$(".signout").show();
}
function destroy_session(){
sessionStorage.removeItem('user');
sessionStorage.removeItem('pass');
$("form.signout")[0].submit();
}
function load_doodles(){
let u = sessionStorage.getItem('user');
let p = sessionStorage.getItem('pass');
$.ajax({
url: 'index.php',
type: 'POST',
data: {
'req': 'load',
'user': u,
'pass': p
},
dataType: 'json',
success: function(data){
//debug("ajax returned "+ JSON.stringify(data));
List = data;
update_list();
},
error: function(request,error){
debug("ajax error "+ error);
}
});
}
function save(){
let u = sessionStorage.getItem('user');
let p = sessionStorage.getItem('pass');
$.ajax({
url: 'index.php',
type: 'POST',
data: {
'req': 'save',
'user': u,
'pass': p,
'name': Doodle.name,
'drawing': Doodle.info
},
dataType: 'text',
success: function(data){
debug("ajax returned "+ data);
},
error: function(request,error){
debug("ajax error "+ error);
}
});
load_doodles();
update();
return false;
}
function new_doodle(){
Doodle.drawing = [];
Doodle.name = "doodle" + (List.length + 1);
update();
return false;
}
function moveForward(){
doodle_add_element('F');
return false;
}
function moveBack(){
doodle_add_element('B');
return false;
}
function turnLeft(){
doodle_add_element('L');
return false;
}
function turnRight(){
doodle_add_element('R');
return false;
}
function focusAngle(){
$("#angle").focus();
return false;
}
function setAngle(){
doodle_add_mod( $("#angle")[0] );
}
function focusStep(){
$("#step").focus();
return false;
}
function setStep(){
doodle_add_mod( $("#step")[0] );
}
function showTurtle(){
doodle_add_mod( { 'id': 'turtle', 'value': true } );
$("#Tlink").hide();
$("#Nlink").show();
return false;
}
function hideTurtle(){
doodle_add_mod( { 'id': 'turtle', 'value': false } );
$("#Tlink").show();
$("#Nlink").hide();
return false;
}
function undo(){
if( Doodle.drawing.length > 0 ){
Doodle.undo.push(Doodle.drawing.pop());
}
update();
return false;
}
function redo(){
if( Doodle.undo.length > 0 ){
Doodle.drawing.push(Doodle.undo.pop());
}
update();
return false;
}
function update(){
update_doodle_info();
update_undo_info();
update_drawing();
save_doodle_locally();
}
function save_doodle_locally(){
sessionStorage.setItem('doodle', JSON.stringify( Doodle ) );
}
function load_local_doodle(){
let x = sessionStorage.getItem('doodle');
if( x !== null ){
Doodle = JSON.parse( x );
debug(" loaded doodle, type: " + typeof( Doodle ) );
} else {
new_doodle();
}
}
function update_list(){
$(".list").html("");
List.forEach(function(val, idx){
$(".list").append(
"<li><a href=# onClick='return choose("+ idx +")'>"+
val.name +" "+
add_breaking_spaces( val.drawing ) +"</a> "+
"<a href=# onClick='return deletedoodle("+ val.id +")' >delete</a>"
);
});
}
function deletedoodle( idx ){
let u = sessionStorage.getItem('user');
let p = sessionStorage.getItem('pass');
$.ajax({
url: 'index.php',
type: 'POST',
data: {
'req': 'delete',
'user': u,
'pass': p,
'doodle': idx
},
dataType: 'text',
success: function(data){
debug("ajax returned "+ data);
load_doodles();
},
error: function(request,error){
debug("ajax error "+ error);
}
});
}
function choose( idx ){
Doodle.name = List[idx].name;
Doodle.drawing = [];
for( var i = 0; i < List[idx].drawing.length; i++){
switch( List[idx].drawing.charAt(i) ){
case 'F':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_element( { type: 'F', F: s.num } );
break;
case 'B':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_element( { type: 'B', B: s.num } );
break;
case 'L':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_element( { type: 'L', L: s.num } );
break;
case 'R':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_element( { type: 'R', R: s.num } );
break;
case 'T':
showTurtle();
break;
case 'N':
hideTurtle();
break;
case 'A':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_mod( { id: 'angle', value: +s.num } );
break;
case 'S':
var s = read_number( List[idx].drawing, i+1 );
i += s.len;
doodle_add_mod( { id: 'step', value: +s.num } );
break;
}
}
update();
return false;
}
function read_number( str, idx ){
var n = str.substring( idx ).match(/^\d+/);
if( n !== null ){
return { num: Number( n[0] ), len: n[0].length };
} else {
return { num: 1, len: 0 };
}
}
function update_drawing(){
let cx = $('canvas')[0].getContext('2d');
let pt = doodle_path_and_final_turtle();
let p = pt[0];
let t = pt[1];
cx.setTransform( 1, 0, 0, 1, 0, 0 );
cx.clearRect( 0, 0, cx.canvas.width, cx.canvas.height );
cx.beginPath();
cx.strokeStyle = 'blue';
cx.linewidth = 2;
autoscale( cx, p );
draw_path( cx, p );
cx.stroke();
if( t.show ){
draw_turtle( cx, t );
}
}
function autoscale( cx, p ){
let minx = p.map(
s => s.map( v=>v[0] ).reduce( (a, b) => Math.min( a, b ) )
).reduce( (a, b) => Math.min( a, b ) );
let maxx = p.map(
s => s.map( v=>v[0] ).reduce( (a, b) => Math.max( a, b ) )
).reduce( (a, b) => Math.max( a, b ) );
let miny = p.map(
s => s.map( v=>v[1] ).reduce( (a, b) => Math.min( a, b ) )
).reduce( (a, b) => Math.min( a, b ) );
let maxy = p.map(
s => s.map( v=>v[1] ).reduce( (a, b) => Math.max( a, b ) )
).reduce( (a, b) => Math.max( a, b ) );
debug( "x["+minx + ","+maxx + "] y["+miny + ","+maxy + "]" );
minx = Math.min( minx - 20, 0 );
maxx = Math.max( maxx + 20, cx.canvas.width);
miny = Math.min( miny - 20, 0 );
maxy = Math.max( maxy + 20, cx.canvas.height);
let dx = maxx - minx;
let dy = maxy - miny;
let sx = (cx.canvas.width) / dx;
let sy = (cx.canvas.height) / dy;
let s = Math.min( sx, sy );
let ss = Math.min( s, 1 );
//let ss = s * .5;
cx.translate( - minx, - miny ); debug( "translate -" + minx + ", -" + miny );
//cx.translate( minx + dx/2, miny + dy/2 ); debug( "translate " + (minx + dx/2) + ", " + (miny + dy/2) );
cx.scale( ss, ss ); debug( "scale " + ss + ", " + ss );
//cx.translate( (- dx/2)*ss, (- dy/2)*ss ); debug( "translate " + ((- dx/2)*ss) + ", " + ((- dy/2)*ss) );
}
function draw_path( cx, path ){
path.forEach(function(subpath){
cx.moveTo(...subpath[0]);
//debug( "moveto (" + subpath[0][0] + ", " + subpath[0][1] + ")" );
subpath.slice(1).forEach(function(point){
cx.lineTo(...point);
//debug( "lineto (" + point[0] + ", " + point[1] + ")" );
});
});
}
function doodle_path_and_final_turtle(){
let T = Turtle();
let p = [];
let s = [ [ T.xpos, T.ypos ] ];
Doodle.drawing.forEach(function(val){
if( val.type == 'mod') {
if( 'A' in val ){
T.adel = val.A;
}
if( 'S' in val ){
T.step = val.S;
}
if( 'T' in val ){
T.show = val.T;
if( T.show ){
p.push( s );
s = [ [ T.xpos, T.ypos ] ];
}
}
} else {
let t = val.type;
switch( val.type ){
case 'F':
T.xpos += val[t] * T.step * Math.cos( rad( T.angle ) );
T.ypos += val[t] * T.step * Math.sin( rad( T.angle ) );
if( T.show ){ s.push( [ T.xpos, T.ypos ] ); }
break;
case 'B':
T.xpos -= val[t] * T.step * Math.cos( rad( T.angle ) );
T.ypos -= val[t] * T.step * Math.sin( rad( T.angle ) );
if( T.show ){ s.push( [ T.xpos, T.ypos ] ); }
break;
case 'L':
T.angle -= val[t] * T.adel;
break;
case 'R':
T.angle += val[t] * T.adel;
break;
}
}
});
if( s ){
p.push( s );
}
return [ p, T ];
}
function draw_turtle( cx, t ){
cx.translate( t.xpos, t.ypos );
cx.scale( 1.5, 1.5 );
cx.rotate( rad( t.angle ) );
cx.beginPath();
var p = [ [.5, 0], [1, 1], [2, 3], [4, 4], [6, 4], [8, 3], [9, 1], [9.5, 0],
[9,-1], [8,-3], [6,-4], [4,-4], [2,-3], [1,-1],
[2, 0], [3, 3], [3, 0], [3,-3], [4, 2], [4,-2], [6, 2],
[6,-2], [7, 3], [7, 0], [7,-3], [8, 0] ];
var v = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0,
1, 14, 13, 14, 16,
18, 15, 2, 15, 3, 15, 18,
20, 22, 4, 22, 5, 22, 20,
23, 25, 6, 25, 7, 25, 23,
21, 24, 9, 24, 10, 24, 21,
19, 17, 11, 17, 12, 17 ];
cx.moveTo(...p[v[0]]);
for( let i = 1; i < v.length; i++ ){
cx.lineTo(...p[v[i]]);
}
cx.strokeStyle = 'darkgreen';
cx.lineWidth = 1;
cx.stroke();
}
function rad( deg ){
return deg * ( Math.PI / 180.0 );
}
function Turtle(){
return {
xpos: 10,
ypos: 10,
angle: 0,
step: 20,
adel: 90,
show: true,
};
}
function update_doodle_info(){
$("#doodleName").val(Doodle.name);
$(".doodle").html("");
var x = "";
Doodle.drawing.forEach(function(val){
if( val.type == 'mod' ){
if( 'A' in val ){
x += 'A' + val.A;
}
if( 'S' in val ){
x += 'S' + val.S;
}
if( 'T' in val ){
x += val.T ? 'T' : 'N';
}
} else {
if( val[val.type] == 1 ){
x += val.type;
} else {
x += val.type + val[val.type];
}
}
});
Doodle.info = x;
x = add_breaking_spaces( x );
//debug( x );
$(".doodle").html(x);
}
function update_undo_info(){
$(".undo").html("");
var x = "";
Doodle.undo.forEach(function(val){
if( val.type == 'mod' ){
if( 'A' in val ){
x = 'A' + val.A + x;
}
if( 'S' in val ){
x = 'S' + val.S + x;
}
if( 'T' in val ){
x = (val.T ? 'T' : 'N') + x;
}
} else {
if( val[val.type] == 1 ){
x = val.type + x;
} else {
x = val.type + val[val.type] + x;
}
}
});
Doodle.undoinfo = x;
x = add_breaking_spaces( x );
$(".undo").html(x);
}
function add_breaking_spaces( x ){
let y = x.split("");
let i = x.length - 1;
let gap = 4;
i -= i % gap;
//debug( i );
for( ; i > 0; i -= gap ){
//debug( "splice( " + i + ", '<wbr>' );");
y.splice( i, 0, '<wbr>' );
}
let z = y.join("");
//debug( z );
return z;
}
function doodle_add_element( el ){
if( typeof(el) == 'object' ){
if( Doodle.drawing.length > 0 ){
let last = Doodle.drawing[ Doodle.drawing.length - 1 ];
if( last.type == el.type ){
if( el.type == 'mod' ){
Object.assign( last, el );
} else {
last[el.type] += el[el.type];
}
} else {
Doodle.drawing.push( el );
}
} else {
Doodle.drawing.push( el );
}
update();
} else {
//debug( "creating move object" );
let x = { 'type': el };
x[el] = 1;
doodle_add_element( x );
}
}
function doodle_add_mod( whence ){
switch( whence.id ){
case 'angle':
doodle_add_element( { 'type': "mod", 'A': whence.value } );
break;
case 'step':
doodle_add_element( { 'type': "mod", 'S': whence.value } );
break;
case 'turtle':
doodle_add_element( { 'type': "mod", 'T': whence.value } );
break;
}
}
function debug(msg){
let x = $("#debug").html();
$("#debug").html( x + '<br>' + msg );
}
db.php
Connect to database and define functions for interaction. index.php
calls these functions in response to POST
requests. The command line scripts create.php
, delete.php
, list.php
, load.php
also call these functinos. Prepared statements are used throughout to protect the database from injections.
<?php
$host = '127.0.0.1';
$user = '';
$pass = '';
$db = 'doodle';
$charset = 'utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
throw new PDOException($e->getMessage(), (int)$e->getCode());
}
function create_database_tables(){
global $pdo;
$stmt = $pdo->query('CREATE TABLE users (
uid INT NOT NULL AUTO_INCREMENT,
user VARCHAR(100),
email VARCHAR(100),
pass VARCHAR(100),
PRIMARY KEY(uid) );');
if( ! $stmt ){ die("Error creating users table"); }
$stmt = $pdo->query("INSERT INTO users (user, email, pass)
VALUES ('m', '[email protected]', 'm')");
if( ! $stmt ){ die("Error creating user"); }
$sql = $pdo->query('CREATE TABLE doodles (
id INT NOT NULL AUTO_INCREMENT,
uid INT NOT NULL,
name VARCHAR(100),
drawing VARCHAR(512),
created TIMESTAMP,
PRIMARY KEY(id) );');
if( ! $stmt ){ die("Error creating doodles table"); }
$stmt = $pdo->query("INSERT INTO doodles (uid, name, drawing)
VALUES (1, 'doodle1', 'FRFLFRFLF')");
if( ! $stmt ){ die("Error creating doodle"); }
}
function list_database_tables(){
global $pdo;
echo "users:\n";
$stmt = $pdo->query('SELECT * FROM users');
while( $row = $stmt->fetch() ){
print_r($row);
echo "\n";
}
echo "doodles:\n";
$stmt = $pdo->query('SELECT * FROM doodles');
while( $row = $stmt->fetch() ){
print_r($row);
echo "\n";
}
}
function delete_database_tables(){
global $pdo;
$stmt = $pdo->query('DROP TABLE users;');
if( ! $stmt ){ die("Error deleting users table"); }
$stmt = $pdo->query('DROP TABLE doodles');
if( ! $stmt ){ die("Error deleting doodles table"); }
}
function sign_in( $user, $pass ){
global $pdo;
$user = sanitize( $user, 'string' );
$pass = sanitize( $pass, 'string' );
$stmt = $pdo->prepare('SELECT * FROM users WHERE
(user = ? OR email = ?) AND pass = ?;');
$stmt->execute([$user, $user, $pass]);
$row = $stmt->fetch();
if( ! $row ){ die( "No such user or wrong password." ); }
$_SESSION['user'] = $user;
$_SESSION['pass'] = $pass;
}
function sign_up( $user, $mail, $pass ){
global $pdo;
$user = sanitize( $user, 'string' );
$pass = sanitize( $pass, 'string' );
$mail = sanitize( $user, 'email' );
$stmt = $pdo->prepare('INSERT INTO users (user, email, pass)
VALUES (?, ?, ?);');
$status = $stmt->execute([$user, $mail, $pass]);
if( ! $status ){ die("Failed to add user."); }
$_SESSION['user'] = $user;
$_SESSION['pass'] = $pass;
}
function sign_out(){
global $pdo;
}
function save_doodle( $user, $pass, $name, $drawing ){
global $pdo;
$uid = get_uid( $user, $pass );
$stmt = $pdo->prepare('INSERT INTO doodles (uid, name, drawing)
VALUES (?, ?, ?);');
$status = $stmt->execute([$uid, $name, $drawing]);
if( ! $status ){ die("Failed to save drawing."); }
echo "Ok.";
}
function delete_doodle( $user, $pass, $doodle ){
global $pdo;
$uid = get_uid( $user, $pass );
$stmt = $pdo->prepare('DELETE FROM doodles WHERE uid=? AND id=?');
$status = $stmt->execute([$uid, $doodle]);
if( ! $status ){ die("Failed to delete doodle."); }
echo "Ok.". $doodle . " " . $status;
}
function load_doodles( $user, $pass ){
global $pdo;
$uid = get_uid( $user, $pass );
echo json_encode( pdo($pdo, 'SELECT * FROM doodles WHERE uid=?',
[ $uid ] )->fetchAll() );
}
function get_uid( $user, $pass ){
global $pdo;
return pdo( $pdo, 'SELECT (uid) FROM users WHERE user=? AND pass=?;',
[ sanitize($user, 'string'), sanitize($pass, 'string') ]
)->fetch()['uid'];
}
function sanitize( $var, $type ){
switch($type){
case 'string':
return filter_var( $var, FILTER_SANITIZE_STRING );
case 'email':
return filter_var( $var, FILTER_SANITIZE_EMAIL );
}
}
function pdo($pdo, $sql, $args = NULL)
{
if (!$args)
{
return $pdo->query($sql);
}
$stmt = $pdo->prepare($sql);
$stmt->execute($args);
return $stmt;
}
create.php
Script to set up database tables, creating default user 'm' and saving one doodle for 'm'.
<?php
include_once("db.php");
create_database_tables();
delete.php
Script to delete all database tables.
<?php
include("db.php");
delete_database_tables();
list.php
Script to list contents of all database tables.
<?php
include_once("db.php");
list_database_tables();
load.php
Script to simulate the ajax request that script.js:load_doodles() performs. Outputs the exact JSON encoded data that the javascript function will receive.
<?php
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['req'] = 'load';
$_POST['user'] = 'm';
$_POST['pass'] = 'm';
require 'index.php';
Of course I'd love to know if any parts can be improved, but I'm specifically concerned about how to organize all the javascript into manageable pieces.