Invalid Page Name
=htmlspecialchars($slug)?> is not a valid page name.
\p{Lu}\p{Ll}+(?:\p{Lu}\p{Ll}+|\d+)+)\b/u'; const RE_PAGE_LINK = '/(?:\[(?
'; } $line = substr($line, 1); yield $line; continue; } if ($inside_list) { $inside_list = false; yield ''; } if ($inside_pre) { $inside_pre = false; yield ''; } if (starts_with($line, '---')) { yield '
' . $line . ''; continue; } $line = trim($line); if (preg_match(RE_FIGURE_IMAGE, $line, $matches)) { $slug = $matches['slug']; yield "
' . $line . '
'; continue; } } if ($inside_list) { yield ''; } if ($inside_pre) { yield ''; } } private function Linkify($text) { $parts = preg_split(RE_HREF_LINK, $text, -1, PREG_SPLIT_DELIM_CAPTURE); $result = ''; $part = current($parts); while ($part !== false) { $result .= $this->LinkifySlugs($part); $link_text = next($parts); $link_href = next($parts); if ($link_text) { $result .= "$link_text"; } elseif ($link_href) { $result .= "$link_href"; } // Pop the balanced parens capture group/subroutine. next($parts); $part = next($parts); } return $result; } private function LinkifySlugs($text) { $state = $this->state; return preg_replace_callback( RE_PAGE_LINK, function($matches) use(&$state) { $slug = $matches["slug"]; $missing = $state->PageExists($slug) ? '' : 'data-missing'; $href = "?$slug"; if ($action = $matches["action"]) { $href .= "=$action"; } if ($title = $matches["title"]) { return "$title"; } return "$slug"; }, $text, -1, $count, PREG_UNMATCHED_AS_NULL); } private function Inline($text) { return preg_replace( array('/\*([^ ](.*?[^ ])?)\*/', '/"(.+?)"/', '/`(.+?)`/'), array('$1', '$1', '$1
'),
$text);
}
}
function starts_with($string, $prefix) {
return substr($string, 0, strlen($prefix)) == $prefix;
}
session_cache_limiter('none');
session_start();
$state = new State();
switch (strtoupper($_SERVER['REQUEST_METHOD'])) {
case 'GET':
if (empty($_GET)) {
header('Location: ?' . MAIN_PAGE, true, 303);
exit;
}
$slug = filter_input(INPUT_GET, 'slug');
if (empty($slug)) {
if (USE_MULTICOLUMN) {
render_viewer();
exit;
} else {
$slug = array_key_first($_GET);
$action = $_GET[$slug];
if (is_array($action)) {
$id = array_key_first($action);
$action = $action[$id];
$state->title = $slug . ($action ? "[$id]=$action" : "[$id]");
} else {
$id = null;
$state->title = $slug . ($action ? "=$action" : '');
}
}
} else {
$state->title = $slug;
$id = filter_input(INPUT_GET, 'id');
$action = filter_input(INPUT_GET, 'action');
}
ob_start($state);
if (!filter_var($slug, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_PAGE_SLUG]])) {
render_invalid_slug($slug);
exit;
}
if ($id && !filter_var($id, FILTER_VALIDATE_INT)) {
render_invalid_revision($slug, $id);
exit;
}
switch ($slug) {
case RECENT_CHANGES:
$remote_ip = $action;
if ($remote_ip == null) {
view_recent_changes($state, $id);
} elseif (filter_var($remote_ip, FILTER_VALIDATE_IP)) {
view_recent_changes_from($state, $remote_ip, $id);
} else {
render_invalid_address($slug, $remote_ip);
}
return;
}
switch ($action) {
case 'diff':
view_diff_at_revision($state, $slug, $id);
break;
case 'edit':
view_edit($state, $slug, $id);
break;
case 'history':
view_history($state, $slug, $id);
break;
case 'backlinks':
view_backlinks($state, $slug, $id);
break;
case 'text':
$state->render_mode = 'text';
case 'html':
case '':
if ($id) {
view_page_at_revision($state, $slug, $id);
} else {
view_page_latest($state, $slug);
}
break;
case 'image':
if ($id) {
view_image_at_revision($state, $slug, $id);
} else {
view_image_latest($state, $slug);
}
break;
default:
render_invalid_action($slug, $action);
}
ob_end_flush();
break;
case 'POST':
$honeypot = filter_input(INPUT_POST, 'user');
if ($honeypot) {
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed', true, 405);
exit();
}
$body = filter_input(INPUT_POST, 'body');
$time = date('U');
$addr = inet_pton($_SERVER['REMOTE_ADDR']);
$slug = filter_input(INPUT_POST, 'slug');
$image_hash = filter_input(INPUT_POST, 'image_hash');
if (!filter_var($slug, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_PAGE_SLUG]])) {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('Invalid page slug; must be CamelCase123.');
}
$state->pdo->beginTransaction();
$image_file = $_FILES['image_data'];
if (file_exists($image_file['tmp_name']) && is_uploaded_file($image_file['tmp_name'])) {
if ($image_file['error'] > UPLOAD_ERR_OK) {
// File is larger than php.ini's upload_max_filesize.
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('File too big; max allowed is ' . floor(IMAGE_MAX_BYTES / 1024) . 'kb.');
}
$image_file_type = mime_content_type($image_file['tmp_name']);
if (extension_loaded('gd')) {
switch ($image_file_type) {
case 'image/jpeg':
$image = imagecreatefromjpeg($image_file['tmp_name']);
break;
case 'image/png':
$image = imagecreatefrompng($image_file['tmp_name']);
imagepalettetotruecolor($image);
imagealphablending($image, true);
imagesavealpha($image, true);
break;
case 'image/webp':
$image = imagecreatefromwebp($image_file['tmp_name']);
break;
default:
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('Only JPG, PNG, WEBP are accepted.');
}
$image_width = imagesx($image);
if ($image_width > IMAGE_MAX_WIDTH) {
$image = imagescale($image, IMAGE_MAX_WIDTH);
$image_width = imagesx($image);
}
$image_height = imagesy($image);
if ($image_file_type != 'image/webp') {
$image_file_type = 'image/webp';
$image_temp_name = $image_file['tmp_name'] . '.webp';
imagewebp($image, $image_temp_name, 84);
} else {
$image_temp_name = $image_file['tmp_name'];
}
$image_file_size = filesize($image_temp_name);
} elseif (starts_with($image_file_type, 'image/')) {
$image_temp_name = $image_file['tmp_name'];
$image_file_size = $image_file['size'];
} else {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('File is not an image; MIME type must be image/*.');
}
if ($image_file_size > IMAGE_MAX_BYTES) {
// File is larger than our custom limit.
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('File too big; max allowed is ' . floor(IMAGE_MAX_BYTES / 1024) . 'kb.');
}
$statement = $state->pdo->prepare('
INSERT OR IGNORE INTO images (hash, page_slug, content_type, time_created, remote_addr, image_data, image_width, image_height, file_size, file_name)
VALUES (:hash, :page_slug, :content_type, :time_created, :remote_addr, :image_data, :image_width, :image_height, :file_size, :file_name)
;');
$image_hash = sha1_file($image_temp_name);
$file = fopen($image_temp_name, 'rb');
$statement->bindParam('hash', $image_hash, PDO::PARAM_STR);
$statement->bindParam('page_slug', $slug, PDO::PARAM_STR);
$statement->bindParam('content_type', $image_file_type, PDO::PARAM_STR);
$statement->bindParam('time_created', $time, PDO::PARAM_INT);
$statement->bindParam('remote_addr', $addr, PDO::PARAM_STR);
$statement->bindParam('image_data', $file, PDO::PARAM_LOB);
$statement->bindParam('image_width', $image_width, PDO::PARAM_INT);
$statement->bindParam('image_height', $image_height, PDO::PARAM_INT);
$statement->bindParam('file_size', $image_file_size, PDO::PARAM_INT);
$statement->bindParam('file_name', $image_file['name'], PDO::PARAM_STR);
if (!$statement->execute()) {
exit('Unable to upload the image.');
}
} elseif (empty($image_hash)) {
$image_hash = null;
} elseif (!filter_var($image_hash, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_HASH_SHA1]])) {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit('Invalid image_hash; must be SHA1 or empty.');
}
$statement = $state->pdo->prepare('
INSERT INTO revisions (slug, body, time_created, remote_addr, image_hash)
VALUES (:slug, :body, :time, :addr, :image_hash)
;');
$statement->bindParam('slug', $slug, PDO::PARAM_STR);
$statement->bindParam('body', $body, PDO::PARAM_STR);
$statement->bindParam('time', $time, PDO::PARAM_INT);
$statement->bindParam('addr', $addr, PDO::PARAM_STR);
$statement->bindParam('image_hash', $image_hash, PDO::PARAM_STR);
if ($statement->execute()) {
$state->pdo->commit();
$_SESSION['revision_created'] = true;
if (USE_MULTICOLUMN) {
header("Location: ?slug=$slug", true, 303);
} else {
header("Location: ?$slug", true, 303);
}
exit;
}
exit("Unable to create a new revision of $slug.");
}
// Views.
function view_page_latest($state, $slug)
{
$statement = $state->pdo->prepare('
SELECT
id, slug, body, revisions.time_created,
image_hash, image_width, image_height
FROM revisions LEFT JOIN images ON image_hash = hash
WHERE slug = ?
ORDER BY id DESC
LIMIT 1
;');
$statement->execute(array($slug));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Revision');
$page = $statement->fetch();
if (!$page) {
render_page_not_found($slug);
return;
}
$page->Anchor($state);
if ($state->render_mode === 'text') {
render_source($page);
} else {
render_latest($page, $state);
}
}
function view_page_at_revision($state, $slug, $id)
{
$statement = $state->pdo->prepare('
SELECT
id, slug, body, revisions.time_created,
image_hash, image_width, image_height
FROM revisions LEFT JOIN images ON image_hash = hash
WHERE slug = ? AND id = ?
;');
$statement->execute(array($slug, $id));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Revision');
$page = $statement->fetch();
if (!$page) {
render_revision_not_found($slug, $id);
return;
}
$page->Anchor($state);
if ($state->render_mode === 'text') {
render_source($page);
} else {
render_revision($page);
}
}
function view_image_latest($state, $slug)
{
$statement = $state->pdo->prepare('
SELECT hash, content_type, image_data, file_size
FROM images JOIN revisions ON hash == image_hash
WHERE revisions.slug = ?
ORDER BY id DESC
LIMIT 1
;');
$statement->execute(array($slug));
$statement->bindColumn('hash', $image_hash, PDO::PARAM_STR);
$statement->bindColumn('content_type', $content_type, PDO::PARAM_STR);
$statement->bindColumn('image_data', $image_data, PDO::PARAM_LOB);
$statement->bindColumn('file_size', $file_size, PDO::PARAM_INT);
if ($statement->fetch(PDO::FETCH_BOUND)) {
$state->render_mode = $content_type;
header("Content-Type: $content_type");
header("Content-Length: $file_size");
header("ETag: $image_hash");
header('Cache-Control: max-age=' . CACHE_MAX_AGE);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $image_hash) {
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
ob_end_flush();
} else {
ob_end_flush();
fpassthru($image_data);
}
} elseif ($state->PageExists($slug)) {
render_image_not_found($slug);
} else {
render_page_not_found($slug);
}
}
function view_image_at_revision($state, $slug, $id)
{
$statement = $state->pdo->prepare('
SELECT hash, content_type, image_data, file_size
FROM images JOIN revisions ON hash == image_hash
WHERE revisions.slug = ? AND revisions.id = ?
;');
$statement->execute(array($slug, $id));
$statement->bindColumn('hash', $image_hash, PDO::PARAM_STR);
$statement->bindColumn('content_type', $content_type, PDO::PARAM_STR);
$statement->bindColumn('image_data', $image_data, PDO::PARAM_LOB);
$statement->bindColumn('file_size', $file_size, PDO::PARAM_INT);
if ($statement->fetch(PDO::FETCH_BOUND)) {
$state->render_mode = $content_type;
header("Content-Type: $content_type");
header("Content-Length: $file_size");
header("ETag: $image_hash");
header('Cache-Control: max-age=' . CACHE_MAX_AGE);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $image_hash) {
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
ob_end_flush();
} else {
ob_end_flush();
fpassthru($image_data);
}
} elseif ($state->PageExists($slug)) {
render_image_not_found($slug, $id);
} else {
render_revision_not_found($slug, $id);
}
}
function view_edit($state, $slug, $id)
{
$statement = $state->pdo->prepare($id ? '
SELECT
id, slug, body, revisions.time_created,
image_hash, image_width, image_height
FROM revisions LEFT JOIN images ON image_hash = hash
WHERE slug = ? AND id = ?
;' : '
SELECT
id, slug, body, revisions.time_created,
image_hash, image_width, image_height
FROM revisions LEFT JOIN images ON image_hash = hash
WHERE slug = ?
ORDER BY id DESC
LIMIT 1
;');
$statement->execute($id ? array($slug, $id) : array($slug));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Revision');
$page = $statement->fetch();
if (!$page) {
if ($id !== null) {
render_revision_not_found($slug, $id);
return;
}
$page = new Revision($slug);
}
render_edit($page);
}
function view_history($state, $slug, $id)
{
$statement = $state->pdo->prepare('
SELECT
slug, id, time_created, remote_addr, body,
LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id,
LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body
FROM revisions
WHERE slug = ?' . ($id ? ' AND id <= ?' : '') . '
ORDER BY id DESC
;');
$statement->execute($id ? array($slug, $id) : array($slug));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Change');
$changes = $statement->fetchAll();
if (!$changes) {
render_page_not_found($slug);
} else {
render_history($slug, $changes);
}
}
function view_diff_at_revision($state, $slug, $id)
{
$statement = $state->pdo->prepare('
SELECT
slug, id, time_created, remote_addr, body,
LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id,
LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body
FROM revisions
WHERE slug = ?' . ($id ? ' AND id <= ?' : '') . '
ORDER BY id DESC
LIMIT 1
;');
$statement->execute($id ? array($slug, $id) : array($slug));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Change');
$change = $statement->fetch();
if (!$change) {
render_page_not_found($slug);
} else {
render_diff($change);
}
}
function view_backlinks($state, $slug, $id)
{
$statement = $state->pdo->prepare('
SELECT DISTINCT slug
FROM revisions
WHERE slug != ? AND body LIKE ?' . ($id ? ' AND id <= ?' : '') . '
;');
$statement->execute($id ? array($slug, "%$slug%", $id) : array($slug, "%$slug%"));
$references = $statement->fetchAll(PDO::FETCH_OBJ);
render_backlinks($slug, $references);
}
function view_recent_changes($state, $p = 0)
{
if ($p > 0) {
render_invalid_offset(RECENT_CHANGES, $p);
return;
}
$limit = 25;
$statement = $state->pdo->prepare('
SELECT
slug, id, time_created, remote_addr, body,
LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id,
LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body
FROM revisions
ORDER BY id DESC
LIMIT ? OFFSET ?
;');
$statement->execute(array($limit, $limit * $p * -1));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Change');
$changes = $statement->fetchAll();
render_recent_changes($p, $changes);
}
function view_recent_changes_from($state, $remote_ip, $p = 0)
{
if ($p > 0) {
render_invalid_offset(RECENT_CHANGES, $p);
return;
}
$limit = 25;
$statement = $state->pdo->prepare('
WITH recent_changes AS (
SELECT
slug, id, time_created, remote_addr, body,
LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id,
LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body
FROM revisions
)
SELECT *
FROM recent_changes
WHERE remote_addr = ?
ORDER BY id DESC
LIMIT ? OFFSET ?
;');
$statement->execute(array(inet_pton($remote_ip), $limit, $limit * $p * -1));
$statement->setFetchMode(PDO::FETCH_CLASS, 'Change');
$changes = $statement->fetchAll();
render_recent_changes_from($remote_ip, $p, $changes);
}
// Rendering templates.
function render_viewer()
{ ?>
=htmlspecialchars($slug)?> is not a valid page name.
=htmlspecialchars($slug)?>[=htmlspecialchars($id)?>] is not a valid revision.
=htmlspecialchars($action)?> is not a valid action name.
=htmlspecialchars($slug)?>[=htmlspecialchars($p)?>] is not a valid range offset.
=htmlspecialchars($ip)?> is not a valid IP address.
=$slug?> doesn't exist yet. Create?
=$slug?>[=$id?>] doesn't exist.
=$slug?>[=$id?>] doesn't have an image.
=$slug?> doesn't have an image. Edit?
=$change->DiffToHtml()?>