Created
February 23, 2016 19:26
-
-
Save xeoncross/0663defd8560dd48deb1 to your computer and use it in GitHub Desktop.
Simple, PHP-based bookmark system so you can save internet pages. Uses HTTP Digest for Auth and PDO-SQLite extension (built-in).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Place the DB files outside of the public web directory so people don't download it! | |
define('DB', 'links.sq3'); | |
// Number of seconds a user must wait to post another link (false to disable) | |
define('WAIT', false); | |
// Should we enforce IP checking? (false to disable) | |
define('IP_CHECK', false); | |
// Auth password (false to disable) | |
define('PASSWORD', 'mybookmarks'); | |
/** | |
* Map paths to callbacks objects/closures | |
* | |
* @see https://gist.github.com/Xeoncross/5357205 | |
* @param string $path | |
* @param mixed $closure | |
* @return mixed | |
*/ | |
function route($path, $closure = null) | |
{ | |
static $routes = array(); | |
if($closure) return $routes[$path] = $closure; | |
foreach($routes as $route => $closure) { | |
if(preg_match("~^$route$~", $path, $match)) { | |
return call_user_func_array($closure, $match); | |
} | |
} | |
} | |
/** | |
* HTTP Digest Auth is a lot better than plain HTTP Auth since | |
* the password is not transfered in the clear | |
*/ | |
function hmac_http_auth($password, $realm = "Simple Bookmark") | |
{ | |
if( ! empty($_SERVER['PHP_AUTH_DIGEST'])) | |
{ | |
// Decode the data the client gave us | |
$default = array('nounce', 'nc', 'cnounce', 'qop', 'username', 'uri', 'response'); | |
preg_match_all('~(\w+)="?([^",]+)"?~', $_SERVER['PHP_AUTH_DIGEST'], $matches); | |
$data = array_combine($matches[1] + $default, $matches[2]); | |
// Generate the valid response | |
$A1 = md5($data['username'] . ':' . $realm . ':' . $password); | |
$A2 = md5(getenv('REQUEST_METHOD').':'.$data['uri']); | |
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); | |
// Compare with what was sent | |
if($data['response'] === $valid_response) | |
{ | |
return $data['username']; | |
} | |
} | |
// Failed, or haven't been prompted yet | |
header('HTTP/1.1 401 Unauthorized'); | |
header('WWW-Authenticate: Digest realm="' . $realm. | |
'",qop="auth",nonce="' . uniqid() . '",opaque="' . md5($realm) . '"'); | |
exit(); | |
} | |
function db($args = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)) | |
{ | |
static $db; | |
$db = $db ?: (new PDO('sqlite:' . DB, 0, 0, $args)); | |
return $db; | |
} | |
function query($sql, $params = NULL) | |
{ | |
$s = db()->prepare($sql); | |
$s->execute(array_values((array) $params)); | |
return $s; | |
} | |
function insert($table, $data) | |
{ | |
query("INSERT INTO $table(" . join(',', array_keys($data)) . ')VALUES(' | |
. str_repeat('?,', count($data)-1). '?)', $data); | |
return db()->lastInsertId(); | |
} | |
function update($table, $data, $value) | |
{ | |
return query("UPDATE $table SET ". join('`=?,`', array_keys($data)) | |
. "=?WHERE i=?", $data + array($value))->rowCount(); | |
} | |
function delete($table, $field, $value) | |
{ | |
return query("DELETE FROM $table WHERE $field = ?", $value)->rowCount(); | |
} | |
function filter($string) | |
{ | |
return htmlspecialchars(trim(@iconv('UTF-8', 'UTF-8//TRANSLIT//IGNORE', $string))); | |
} | |
function lastLinkByIP($ip) | |
{ | |
return query('SELECT created FROM links WHERE ip = ?', array($ip))->fetchColumn(); | |
} | |
/* @todo | |
function limitPerDay($ip, $start, $end) | |
{ | |
return query('SELECT count(created) FROM links WHERE ip = ? AND created BETWEEN ? AND ?', array($ip, $start, $end))->fetchColumn(); | |
} | |
*/ | |
function getLinkID($url) | |
{ | |
return query('SELECT id FROM links WHERE url = ?', array($url))->fetchColumn(); | |
} | |
function getLink($id) | |
{ | |
return query('SELECT id,title,url,host FROM links WHERE id = ?', array($id))->fetch(); | |
} | |
function getRecentLinks($limit = 20, $offset = 0) | |
{ | |
return query('SELECT id,title,description,url,host FROM links ORDER BY created DESC LIMIT ' . $limit); | |
} | |
function getHTMLMeta($html) | |
{ | |
libxml_use_internal_errors(true); | |
$doc = new DOMDocument(); | |
$doc->loadHTML($html); | |
$xpath = new DOMXPath($doc); | |
return array( | |
@$xpath->query('//head/title')->item(0)->nodeValue, | |
@$xpath->query('/html/head/meta[@name="description"]/@content')->item(0)->value | |
); | |
} | |
function getURL($url) | |
{ | |
// First try to use cURL | |
if(function_exists('curl_init')) { | |
$ch = curl_init($url); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); | |
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13'); | |
$content = curl_exec($ch); | |
curl_close($ch); | |
return $content; | |
} | |
// file_get_contents will follow redirects as of 5.3.2 http://php.net/manual/en/context.http.php | |
return file_get_contents($url); | |
} | |
/* | |
* Start of Application Logic | |
*/ | |
// Create database if it does not exist | |
if( ! is_file(DB)) { | |
query('CREATE TABLE "links" ( | |
id INTEGER PRIMARY KEY, | |
url TEXT, | |
host TEXT, | |
user TEXT, | |
title TEXT, | |
description TEXT, | |
html, TEXT, | |
ip TEXT, | |
created INTEGER | |
)'); | |
} | |
header('Content-Type: text/html; charset="UTF-8"'); | |
$user = null; | |
// If we require passwords | |
if(PASSWORD) { | |
$user = hmac_http_auth(PASSWORD); | |
} | |
define('USER', $user); | |
/* | |
* Page routes | |
*/ | |
// Home page | |
route('/', function($path) | |
{ | |
if(isset($_GET['url'])) { | |
$url = substr($_GET['url'], 0, 200); | |
if( ! filter_var($url, FILTER_VALIDATE_URL)) { | |
//dump($url, $path); | |
die('Invalid ?url=... passed: ' . $url); | |
} | |
if( ! ($host = parse_url($_GET['url'], PHP_URL_HOST))) { | |
die('Invalid ?url=... passed: ' . $url); | |
} | |
// Does this URL already exist? Re-use the ID | |
$id = getLinkID($url); | |
if( ! $id) { | |
$ip = getenv('REMOTE_ADDR'); | |
// If we have rate-limiting on | |
if(WAIT) { | |
$ts = (int) lastLinkByIP($ip); | |
if($ts AND $ts < (time() - WAIT)) { | |
die('You can only post one link every ' . WAIT . ' seconds'); | |
} | |
} | |
// Anti-spam bot check | |
if(IP_CHECK AND $ip != '127.0.0.1') { | |
if(checkdnsrr(join('.',array_reverse(explode('.',$ip))).".opm.tornevall.org","A")) { | |
die('Your IP is blacklisted as a bot'); | |
} | |
} | |
// If we can't load the page, then the server must be down | |
if( ! ($html = getURL($url))) { | |
die('Unable to get ?url=... passed: ' . $url); | |
} | |
list($title, $description) = getHTMLMeta($html); | |
$id = insert('links', array( | |
'ip' => $ip, | |
'url' => $url, | |
'host' => $host, | |
'user' => USER, | |
'title' => filter($title), | |
'description' => filter($description), | |
'html' => filter($html), | |
'created' => time() | |
)); | |
} | |
// 307 Temporary Redirect | |
$slug = base_convert($id, 10, 32); | |
header("Location: /$slug", 0, 307); | |
exit($slug); | |
} | |
?> | |
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"/> | |
<title>Simple URL Shortener</title> | |
<style type="text/css"> | |
body { font: 1em/1.4em serif, georgia; } | |
a { text-decoration: none; } | |
a:hover { text-decoration: underline; } | |
h1 { font-weight: normal;} | |
div { margin: 1em auto; max-width: 700px;} | |
p { padding: 0; margin: 0;} | |
li { padding-bottom: 1em;} | |
</style> | |
</head> | |
<body lang="en"> | |
<div> | |
<h1>Simple URL shortener</h1> | |
<p> | |
<a href="javascript:location.href='http://<?php print getenv('HTTP_HOST'); ?>/?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title);"> | |
Bookmarklet (drag to your browser) | |
</a> | |
</p> | |
<p>Recently people saved:</p> | |
<ul> | |
<?php | |
foreach(getRecentLinks(80) as $link) { | |
$slug = base_convert($link['id'], 10, 32); | |
print '<li><span><a href="' . $link['url'] . '" rel="nofollow">' . $link['title'] . "</a>"; | |
print ' (<a href="/' . $slug . '+">' . $slug . "</a>)</span>\n"; | |
print '<p>' . $link['description'] . "</p></li>\n"; | |
} ?> | |
</ul> | |
</div> | |
</body> | |
</html> | |
<?php | |
}); | |
// A page link | |
route('/(\w+)(\+?)', function($path, $slug, $info = false) | |
{ | |
if($id = base_convert($slug, 32, 10)) { | |
if($link = getLink($id)) { | |
// Show extra info? | |
if($info) { ?> | |
<script> | |
link_refresh=window.setTimeout(function(){window.location.href='<?php print $link['url']; ?>'},4000); | |
</script> | |
<noscript> | |
<meta http-equiv="refresh" content="4; url=<?php print $link['url']; ?>" /> | |
</noscript> | |
Redirecting you to... <a href="<?php print $link['url']; ?>"><?php print $link['title']; ?></a> | |
<br><br> | |
(<i>You can check Google to see if there is a problem with | |
<a href="http://www.google.com/safebrowsing/diagnostic?site=<?php print $link['url']; ?>"> | |
<?php print $link['url']; ?> | |
</a></i>) | |
<?php /* | |
<br> | |
<pre> | |
<?php print $link['html']; ?> | |
</pre> | |
*/ | |
?> | |
<?php } else { | |
// 301 Moved Permanently | |
header('Location: ' . $link['url'], 0, 301); | |
var_dump($link); | |
} | |
exit(); | |
} | |
} | |
print 'Unknown Link ID'; | |
}); | |
// Catch all fun | |
route('.+', function($path) | |
{ | |
header("HTTP/1.0 404 Not Found"); | |
header('Location: /', 0, 301); | |
exit(); | |
}); | |
route(parse_url(getenv('REQUEST_URI'), PHP_URL_PATH)); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment