commit 62fa3d6b6f6293ddc96a7b47b280407ca1e3f642
Author: Claudio Alessi <smoppy@gmail.com>
Date:   Tue,  8 Dec 2015 22:05:04 +0100
init
Diffstat:
| A | README.md |  |  | 132 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | bored.php |  |  | 539 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
2 files changed, 671 insertions(+), 0 deletions(-)
diff --git a/README.md b/README.md
@@ -0,0 +1,132 @@
+Bored is a PHP micro-framework. It provides a simple framework but does not
+enforce any particular structure so you can bring things together in order to
+refine your own ad-hoc framework.
+
+In a nutshell:
+```
+include('bored.php');
+
+route('GET', '/hello/!name', function($name) {
+	return "Hello ${name}";
+});
+
+bored_run();
+```
+
+The main documentation is the source code.
+
+### Concepts
+Things bored will never have:
+
+* An ORM
+* Dependency injection
+* Templates system
+* Built-in database migration
+* Unit testing
+* More than 2000 SLOC.
+
+### Routing
+Routing handling. It works like this:
+
+```
+route('GET', '/hello/!name/?surname', function($name, $surname = '') {
+	$s = "Hello $name";
+	if($surname)
+		$s .= " $surname";
+	return $s;
+});
+```
+
+Argument prefixes means !mandatory and ?optional.
+
+### Database
+Database facilities are based on the mysqli PHP extension. It mainly consists
+of a single function dbquery() which allows few but very convenient ways to
+interact with the database. Some example:
+
+Fetch a single row:
+```
+$sql = "select id,username from users where id = 30";
+$user = dbquery($sql);
+print_r($user);
+```
+
+This will results in the following:
+
+```
+Array
+(
+    [id] => 30
+    [username] => userfoo
+    ...
+)
+```
+
+Fetch multiple rows:
+```
+$sql = "select id,username from users";
+$users = dbquery($sql, -1);
+print_r($users);
+```
+
+This produces an output like this:
+
+```
+Array
+(
+    [0] => Array
+        (
+            [id] => 248
+            [username] => userbar
+            ...
+        )
+
+    [1] => Array
+        (
+            [id] => 425
+            [username] => userbaz
+            ...
+        )
+    ...
+)
+```
+
+### Views
+...
+
+### Session
+...
+
+### Run-time cache
+...
+
+### Utils
+
+#### Mails
+...
+
+#### Output formatting
+...
+
+### Web server configuration
+I can't cover all scenarious, here is the most common where you a bored
+instance for a single domain:
+
+#### nginx
+
+```
+server {
+        root /usr/share/nginx/www/bored/app/public/;
+        index index.html index.php;
+        server_name bored;
+
+        location / {
+                try_files $uri $uri/ /index.php?$args;
+                location ~ \.php$ {
+			fastcgi_pass unix:/var/run/php5-fpm.sock;
+                        fastcgi_param SCRIPT_FILENAME $request_filename;
+                        include fastcgi_params;
+                }
+        }
+}
+```
diff --git a/bored.php b/bored.php
@@ -0,0 +1,539 @@
+<?php
+
+define('PATH_VIEWS', 'views');
+define('DEFAULT_LAYOUT', 'main');
+
+$HT_DEFAULT_FMTS = [
+	'year_0' => "An year ago",
+	'years_0' => "%d years ago",
+	'year_1' => "An year from now",
+	'years_1' => "%d years from now",
+	'month_0' => "A month ago",
+	'months_0' => "%d months ago",
+	'month_1' => "A month from now",
+	'months_1' => "%d months from now",
+	'day_0' => "A day ago",
+	'days_0' => "%d days ago",
+	'day_1' => "A day from now",
+	'days_1' => "%d days from now",
+	'hour_0' => "An hour ago",
+	'hours_0' => "%d hours ago",
+	'hour_1' => "An hour from now",
+	'hours_1' => "%d hours from now",
+	'minute_0' => "A minute ago",
+	'minutes_0' => "%d minutes ago",
+	'minute_1' => "A minute from now",
+	'minutes_1' => "%d minutes from now",
+	'second_0' => "A second ago",
+	'seconds_0' => "%d seconds ago",
+	'second_1' => "A second from now",
+	'seconds_1' => "%d seconds from now",
+];
+
+$HT_DIVS = [
+	'year' => 60*60*24*365,
+	'month' => 60*60*24*31,
+	'day' => 60*60*24,
+	'hour' => 60*60,
+	'minute' => 60,
+	'second' => 0
+];
+
+$dblink = null;
+
+function route($method, $route, $func = null) {
+        static $routes = [];
+        if(!$func) {
+                $r = null;
+                $n = '';
+                $argv = [];
+                foreach(explode('/', $route) as $arg) {
+                        $n .= ($n == '/' ? $arg : "/$arg");
+                        if($r)
+                                $argv[] = $arg;
+			if(isset($routes[$method][$n])) {
+                                $r = $routes[$method][$n];
+				$argv = [];
+				if($route == $n)
+					break;
+			}
+			if(isset($routes[$method]["$n/"])) {
+                                $r = $routes[$method]["$n/"];
+				$argv = [];
+			}
+                }
+		if(!$r || (count($argv) < $r['mandatory']) || (count($argv) > $r['argc'])) {
+			header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found'); 
+			header("Status: 404 Not Found"); /* CGI */
+			exit(1);
+		}
+                return call_user_func_array($r['func'], $argv);
+        }
+        $name = [];
+        $argc = 0;
+        $mandatory = 0;
+        foreach(explode('/', $route) as $arg) {
+                switch(@$arg[0]) {
+                        case '!': ++$argc; ++$mandatory; break;
+                        case '?': ++$argc; break;
+                        default: $name[] = $arg; break;
+                }
+        }
+        $name = implode('/', $name);
+	if($argc)
+		$name .= '/';
+        $routes[$method][$name] = ['func' => $func, 'argc' => $argc, 'mandatory' => $mandatory];
+        return 0;
+}
+
+function dbopen($host, $user, $pass, $dbname) {
+	if(!($r = @mysqli_connect($host, $user, $pass, $dbname)))
+		die('database error');
+	global $dblink;
+	$dblink = $r;
+	return $r;
+}
+
+function dbquery($sql, $limit = 1, $multi = false) {
+	if(!is_string($sql) || !($sql = trim($sql)))
+		return false;
+
+	$ck = md5($sql);
+	if(($ret = cache($ck)))
+		return $ret;
+
+	global $dblink;
+	$cn = "$sql-$limit-$multi";
+
+	$cmd = strtolower(substr($sql, 0, strpos($sql, ' ')));
+
+	if($cmd == 'select') {
+		if($limit == -1)
+			$limit = '18446744073709551615';
+		$sql .= " limit $limit";
+	}
+
+	if($multi)
+		$res = mysqli_multi_query($dblink, $sql);
+	else
+		$res = mysqli_query($dblink, $sql);
+	if(!$res)
+		return false;
+
+	if($multi) {
+		$ret = [];
+
+		for($res = mysqli_use_result($dblink); $res; $res = mysqli_store_result($dblink)) {
+			$r = [];
+			while(($t = mysqli_fetch_assoc($res)))
+				$r[] = $t;
+			$ret[] = $r;
+			mysqli_free_result($res);
+			mysqli_next_result($dblink);
+		}
+	}
+	else {
+		switch($cmd) {
+			case 'select':
+			case 'call':
+				if($cmd == 'select' && $limit == '1') {
+					$ret = mysqli_fetch_assoc($res);
+					break;
+				}
+
+				$ret = [];
+				while(($t = mysqli_fetch_assoc($res)))
+					$ret[] = $t;
+				break;
+
+			case 'insert':
+				$ret = mysqli_insert_id($dblink);
+				if(!$ret)
+					$ret = true;
+				break;
+
+			case 'delete':
+				$ret = mysqli_affected_rows($dblink);
+				break;
+
+			default:
+				$ret = $res;
+				break;
+		}
+	}
+
+	return $ret;
+}
+
+function dbping($l = null) {
+        if(!$l) {
+                global $dblink;
+                $l = $dblink;
+        }
+	return mysqli_ping($l);
+}
+
+function dberror($l = null) {
+        if(!$l) {
+                global $dblink;
+                $l = $dblink;
+        }
+        return mysqli_error($l);
+}
+
+function dbin($s) {
+	return addslashes(htmlentities($s, ENT_QUOTES, 'UTF-8'));
+}
+
+function dbout($s) {
+	return stripslashes(html_entity_decode($s, ENT_QUOTES, 'UTF-8'));
+}
+
+function dbins($tbl, $info) {
+	$vals = [];
+	foreach($info as $k => $v) {
+		$v = dbin($v);
+		$vals["`$k`"] = "'$v'";
+	}
+	$sql = 'insert into `'.$tbl.'`
+		('.implode(',', array_keys($vals)).')
+		values ('.implode(',', array_values($vals)).')';
+	return dbquery($sql);
+}
+
+function dbupd($tbl, $info, $id) {
+	$vals = [];
+	foreach($info as $k => $v) {
+		$v = dbin($v);
+		$vals[] = "`$k`= '$v'";
+	}
+	$vals = implode(',', $vals);
+	$sql = "update `$tbl` set $vals where id = $id";
+	return dbquery($sql);
+}
+
+function dbdel($tbl, $id) {
+	$sql = "delete from `$tbl` where id = $id";
+	return dbquery($sql);
+}
+
+function sizefitbox($src, $dst) {
+	list($ow, $oh) = explode('x', $src);
+	list($tow, $toh) = explode('x', $dst);
+
+	$rw = $tow / $ow;
+	$rh = $toh / $oh;
+	$ratio = min($rw, $rh);
+
+	$w = $ow * $ratio;
+	$h = $oh * $ratio;
+
+	$w = (int)$w;
+	$h = (int)$h;
+
+	return "${w}x${h}";
+}
+
+function thumb($src, $size = '64x64', $force = 0) {
+	if(strpos($size, 'x') === false)
+		$size = (int)$size.'x'.(int)$size;
+	$pi = pathinfo($src);
+	/* Since imgresize() use the extension to identify the file type, it's
+	 * faster to returns here. */
+	if(!@$pi['extension'])
+		return null;
+	/* name.ext > name.size.ext */
+	$thumb = "${pi['dirname']}/${pi['filename']}.${size}.${pi['extension']}";
+	if(!$force && file_exists($thumb))
+		return $thumb;
+	$t = imgresize($src, $thumb, $size);
+	if($t)
+		return $t;
+	return $thumb;
+}
+
+function imgresize($src, $saveas, $whxy = '64x64-0,0', $opts = null) {
+	$in = null;
+	$out = null;
+	$transparency = false;
+	$ext = strtolower((string)@pathinfo($src)['extension']);
+	switch($ext) {
+		case 'jpg':
+		case 'jpeg':
+			$in = 'imagecreatefromjpeg';
+			$out = 'imagejpeg';
+			break;
+		case 'gif':
+			$in = 'imagecreatefromgif';
+			$out = 'imagegif';
+			/* imagegif() doesn't take a third param */
+			if($opts !== null)
+				$opts = null;
+			break;
+		case 'bmp':
+			$in = 'imagecreatefromwbmp';
+			$out = 'imagewbmp';
+			break;
+		case 'png':
+			$in = 'imagecreatefrompng';
+			$out = 'imagepng';
+			$transparency = true;
+			break;
+		default: /* unsupported image */ return -1;
+	}
+	if(!($oi = $in($src)))
+		return 2;
+
+	$t = explode('-', $whxy);
+	$wh = $t[0];
+	$wh = explode('x', $wh);
+	$xy = isset($t[1]) ? $t[1] : '0,0';
+	$xy = explode(',', $xy);
+
+	$w = (int)@$wh[0];
+	$h = isset($wh[1]) ? $wh[1] : $w;
+	$x = (int)@$xy[0];
+	$y = (int)@$xy[1];
+
+	list($iw, $ih) = getimagesize($src);
+	$ratio = [$iw / $ih, $w / $h];
+	if($x != 0 || $y != 0) {
+		$crop = imagecreatetruecolor($w, $h);
+		$cropW = $w;
+		$cropH = $h;
+
+		imagecopy($crop, $oi, 0, 0, (int)$x, (int)$y, $w, $h);
+		if($transparency) {
+			imagealphablending($crop, false);
+			imagesavealpha($crop, true);  
+		}
+	}
+	else if($ratio[0] != $ratio[1]) {
+		$scale = min((float)($iw / $w), (float)($ih / $h));
+		$cropX = (float)($iw - ($scale * $w));
+		$cropY = (float)($ih - ($scale * $h));
+		$cropW = (float)($iw - $cropX);
+		$cropH = (float)($ih - $cropY);
+		$crop = imagecreatetruecolor($cropW, $cropH);
+		if($transparency) {
+			imagealphablending($crop, false);
+			imagesavealpha($crop, true);  
+		}
+		imagecopy($crop, $oi, 0, 0, (int)($cropX / 2), (int)($cropY / 2), $cropW, $cropH);
+	}
+	$ni = imagecreatetruecolor($w, $h);
+	if($transparency) {
+		imagealphablending($ni, false);
+		imagesavealpha($ni, true);  
+	}
+	if(isset($crop)) {
+		imagecopyresampled($ni, $crop, 0, 0, 0, 0, $w, $h, $cropW, $cropH);
+		imagedestroy($crop);
+	}
+	else {
+		imagecopyresampled($ni, $oi, 0, 0, 0, 0, $w, $h, $iw, $ih);
+	}
+	imagedestroy($oi);
+	if($opts !== null)
+		$r = $out($ni, $saveas, $opts);
+	else
+		$r = $out($ni, $saveas);
+	imagedestroy($ni);
+	if($r === false)
+		return 1;
+	return 0;
+}
+
+function jsonerr($v) {
+	json('ko', $v);
+}
+
+function jsonok($v) {
+	json('ok', $v);
+}
+
+function json($state, $res) {
+	die(json_encode(['state' => $state, 'res' => $res]));
+}
+
+function sendmail($from, $to, $subj, $message, $files = null) {
+	ini_set('sendmail_from', $from);
+	$headers =      "From: $from\n" .
+			"Return-Path: <$from>\r\n" .
+			"MIME-Version: 1.0\n";
+
+	if(!(is_array($files) || count($files))) {
+		$headers .=     "Content-Type: text/html; charset=\"UTF-8\"\n" .
+				"Content-Transfer-Encoding: 7bit\n\n";
+	}
+	else {
+		$semi_rand = md5(time());
+		$mime_boundary = "==Multipart_Boundary_x{$semi_rand}x";
+		$headers .=     "Content-Type: multipart/mixed; boundary=\"{$mime_boundary}\"\n";
+		$message =      "--{$mime_boundary}\n" .
+				"Content-Type: text/plain; charset=\"UTF-8\"\n" .
+				"Content-Transfer-Encoding: 7bit\n\n" .
+				$message . "\n\n";
+		foreach($files as $fn) {
+			$f = basename($fn);
+			if(!is_file($fn))
+				continue;
+			$data = chunk_split(base64_encode(file_get_contents($fn)));
+
+			$message .=     "--{$mime_boundary}\n" .
+					"Content-Type: application/octet-stream; name=\"$f\"\n" .
+					"Content-Description: $f\n" .
+					"Content-Disposition: attachment; filename=\"$f\"; size=".filesize($fn).";\n" .
+					"Content-Transfer-Encoding: base64\n\n" . $data . "\n\n";
+		}
+		$message .= "--{$mime_boundary}--";
+	}
+
+	return @mail($to, $subj, $message, $headers);
+}
+
+function _store(&$to, $k, $v) {
+	$r = isset($to[$k]) ? $to[$k] : null;
+	if($v !== null)
+		$to[$k] = $v;
+	return $r;
+}
+
+function cache($k, $v = null) {
+	static $__cache = [];
+	return _store($__cache, $k, $v);
+}
+
+function sess($k, $v = null) {
+	$r = isset($_SESSION[$k]) ? $_SESSION[$k] : null;
+	return _store($_SESSION, $k, $v);
+}
+
+function pre($d) {
+	echo '<pre>';
+	print_r($d);
+	echo '</pre>';
+}
+
+function humanstime($timestamp, $fmts = null) {
+	global $HT_DIVS;
+
+	$divs = $HT_DIVS;
+	$diff = time() - $timestamp;
+	$isfuture = ($diff < 0);
+	if($isfuture)
+		$diff = -$diff;
+
+	foreach($divs as $name => $delta) {
+		if($diff >= $delta)
+			break;
+	}
+
+	$unit = $delta ? $diff / $delta : $diff;
+	$ht = [
+		'name' => $name.($unit > 1 ? 's' : ''),
+		'unit' => $unit,
+		'isfuture' => $isfuture
+	];
+
+	if(!$fmts) {
+		global $HT_DEFAULT_FMTS;
+		$fmts = $HT_DEFAULT_FMTS;
+	}
+
+	$k = $ht['name'].'_'.(int)$ht['isfuture'];
+	return sprintf($fmts[$k], $ht['unit']);
+}
+
+function prepare_form() {
+	$key = key($_FILES);
+	if(!$key)
+		return;
+	$files = @$_FILES[$key];
+	if(@$_POST[$key]) {
+		$files = array_merge($files, $_POST[$key]);
+		unset($_POST[$key]);
+	}
+	$ret = [];
+	foreach($files as $k => $unused) {
+		foreach($files[$k] as $i => $v) {
+			if(!isset($ret[$i]))
+				$ret[$i] = [];
+			$ret[$i][$k] = $v;
+		}
+	}
+	$_FILES = $ret;
+}
+
+function buildhier($items, $pk = 'parent_id', $ck = 'id', $subk = 'children') {
+	$map = [];
+	foreach($items as $k => &$item) {
+		$c = $item[$ck];
+		$map[$c] = &$item;
+	}
+	unset($item);
+	foreach($items as $item) {
+		$p = $item[$pk];
+		if(!$p)
+			continue;
+		$c = $item[$ck];
+		if(!isset($map[$p][$subk]))
+			$map[$p][$subk] = [];
+		$map[$p][$subk][$c] = $map[$c];
+		$map[$c]['__rm'] = 1;
+		$map[$c] = &$map[$p][$subk][$c];
+	}
+	foreach($items as $k => $item)
+		if(@$item['__rm'])
+			unset($items[$k]);
+	return $items;
+}
+
+function viewinc($name, $data = []) {
+	if(!defined('PATH_VIEWS'))
+		die('PATH_VIEWS not defined');
+        foreach($data as $k => $v)
+                ${$k} = $v;
+        $view = PATH_VIEWS.'/'.implode('/', explode('.', $name)).'.php';
+        ob_start();
+        require($view);
+        $d = ob_get_contents();
+        ob_end_clean();
+        return $d;
+}
+
+function viewlinc($name, $data = [], $layout = null, $layoutdata = []) {
+	if(!$layout) {
+		if(!defined('DEFAULT_LAYOUT'))
+			die('DEFAULT_LAYOUT not defined');
+		$layout = DEFAULT_LAYOUT;
+	}
+	$layoutdata['content'] = viewinc($name, $data);
+	return viewinc($layout, $layoutdata);
+}
+
+function view($name, $data = [], $layout = null, $layoutdata = []) {
+	return viewlinc($name, $data, $layout, $layoutdata);
+}
+
+/* called by the app */
+function bored_run() {
+	echo route($_SERVER['REQUEST_METHOD'], (string)@explode('?', $_SERVER['REQUEST_URI'])[0]);
+}
+
+function bored_init() {
+	prepare_form();
+	session_start();
+	if(defined('DBHOST') && defined('DBUSER') && defined('DBPASS') && defined('DBNAME'))
+		dbopen(DBHOST, DBUSER, DBPASS, DBNAME);
+	register_shutdown_function(function() {
+		global $dblink;
+		if(!$dblink)
+			return;
+		mysqli_close($dblink);
+		session_write_close();
+	});
+}
+
+bored_init();
+?>