Skip to content

Instantly share code, notes, and snippets.

@xeoncross
Created February 23, 2016 19:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save xeoncross/0663defd8560dd48deb1 to your computer and use it in GitHub Desktop.
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).
<?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