Files
xxx/get_data.lib.php
tim 8d6282348c Show no date for a story without one.
Such a story showed the date of the story before it, or caused a warning when it came first.
2026-09-26 15:27:51 +02:00

502 lines
15 KiB
PHP

<?php
require_once('open_db.lib.php');
require_once(__DIR__ . '/lib/purify.inc.php');
require_once(__DIR__ . '/html.lib.php');
// The longest story HTML, in bytes. HTML Purifier needs seconds for a few megabytes.
define('MAX_STORY_BYTES', 65536);
function story_too_long($story) {
return strlen((string)$story) > MAX_STORY_BYTES;
}
function display_stories($id = NULL) {
global $db, $is_admin, $editable, $action;
if (is_null($id))
$id = get_id();
$search = get_search_args();
$page = '';
$nolink = NULL;
$posted = FALSE;
$date = '';
if ($action == 'preview') {
global $file_src;
$date = 'Awaiting posting';
$result = get_post_data(TRUE);
$nolink = TRUE;
$posted = TRUE;
} else if ($action == 'submit') {
$date = 'Posted '.gmdate("Y-m-d H:i");
if (!$is_admin) {
$result = get_post_data(TRUE);
$nolink = TRUE;
$posted = TRUE;
} else {
$result = get_query($id);
}
} else {
$result = get_query($id);
}
if (count($result) == 0) {
$page .= ''."\n";
if ($search) {
$page .= '<div class="box">'."\n";
$page .= '<p>No articles found.</p>
<p>Try searching again, or return to the <a
href="/">main page</a>. The search is case-insensitive, and
searches the title, author, date, and the text of each article.</p>'."\n";
$page .= '</div>'."\n";
} else {
$page .= '<div class="box">'."\n";
$page .= '<p>No articles found.</p>';
$page .= '</div>'."\n";
}
$page .= ''."\n";
return $page;
}
$page .= '<ul class="article-list">'."\n";
foreach ($result as $entry) {
if (empty($entry['id']) && isset($_SESSION['allow_queue_image'])) {
$entry['id'] = $_SESSION['allow_queue_image'];
}
// Escape everything that goes into the page
$entry_id = (int)$entry['id'];
$queue_url = $entry['queue_url'] ? 1 : 0;
$title = h($entry['title']);
$entry_date = $date;
if ($entry['date'])
$entry_date = '<time datetime="'.date("Y-m-d\TH:i:sP", strtotime($entry['date'])).'">' .
date("Y-m-d H:i", strtotime($entry['date'])) . '</time>';
$page .= '<li class="box">';
$page .= '<article>';
if ($is_admin && !$editable) {
$page .= '<span class="right">[<a href="/publish/edit?id='.$entry_id.'&amp;queue='.$queue_url.'">edit</a>]</span>';
}
$page .= '<h3 class="articletitle">';
if ($nolink) {
$page .= $title;
} else if ($is_admin && $editable) {
$page .= '<a href="/publish/edit?id='.$entry_id.'&amp;queue='.$queue_url.'">';
$page .= $title;
$page .= '</a>';
} else {
$page .= '<a href="/article?id='.$entry_id.'">';
$page .= $title;
$page .= '</a>';
}
$page .= '</h3>';
$page .= '<dl>';
if ($entry['hidden'])
$page .= '<span class="right red">[hidden]</span>';
$page .= '<dt class="author">By</dt> <dd class="author">' . h($entry['author']) . '</dd>';
$page .= '<dt class="date">Posted</dt> <dd class="date">' . $entry_date . '</dd>';
$page .= '</dl>';
if (isset($file_src)) {
$page .= '<img src="'.h($file_src).'" alt="'.$title.'" />';
} else if (!empty($entry['img']) || !empty($entry['uploaded'])) {
if ($editable) {
$page .= '<img src="/display?id='.$entry_id.'&amp;queue='.$queue_url.'" alt="'.$title.'" />';
} else {
$page .= '<img src="/display?id='.$entry_id.'" alt="'.$title.'" />';
}
} else if ($posted && story_too_long($entry['story'])) {
$page .= '<p>The story is too long to show.</p>'."\n";
} else {
$page .= purify_story($entry['story'])."\n";
}
$page .= "</article>\n</li>\n";
}
$page .= '</ul>'."\n";
if (count($result) > 1)
$page .= get_pager();
return $page;
}
/*
* Return number of entries matching our request
*/
function get_count() {
global $db, $is_admin;
$search = get_search_args();
$queue = get_queue();
$table = which_table();
$params = array();
// if search= is defined, search....
if ($search) {
$count_query = "SELECT COUNT(*) FROM $table WHERE".
" (title LIKE ? OR author LIKE ? OR date LIKE ? OR story LIKE ?)";
$params = array_fill(0, 4, "%$search%");
// Otherwise, fall back to default view
} else {
$count_query = "SELECT COUNT(*) FROM ".$table." WHERE 1";
}
if (!$is_admin)
$count_query .= " AND hidden IS NOT TRUE";
//echo $count_query." <br />";
$row_count = NULL;
if ($count_query) {
if ($result = $db->execute_query($count_query, $params)) {
$row_count = $result->fetch_row();
$result->close();
} else {
db_fail();
}
}
return $row_count[0];
}
/*
* The story columns that the pages read. The image data stays in the
* database (display.php reads it): img is 1 for a story with an image,
* and NULL for one without.
*/
define('STORY_COLUMNS', 'id, title, author, email, date, story, hidden, mime, comments, editor, IF(LENGTH(img) > 0, 1, NULL) AS img');
/*
* Return array of results, correctly keyed to column names.
*/
function get_query($id = NULL) {
global $db, $is_admin;
$search = get_search_args();
$sort = get_sort_arg();
$order = get_order();
$start = get_start_num();
$show = get_show_num();
$queue = get_queue();
$table = which_table();
$results = array();
$params = array();
$limit = " LIMIT ".(int)$start." , ".(int)$show;
// If id= is defined, show the corresponding application only
if (is_numeric($id)) {
if (!$is_admin)
$query = "SELECT ".STORY_COLUMNS." FROM $table WHERE 1 AND hidden IS NOT TRUE AND id = ".(int)$id." LIMIT 0, 1";
else
$query = "SELECT ".STORY_COLUMNS." FROM $table WHERE 1 AND id = ".(int)$id." LIMIT 0, 1";
// if search= is defined, search....
} else if ($search) {
$query = "SELECT ".STORY_COLUMNS." FROM $table WHERE".
" (title LIKE ? OR author LIKE ? OR date LIKE ? OR story LIKE ?)";
$params = array_fill(0, 4, "%$search%");
if (!$is_admin)
$query .= " AND hidden IS NOT TRUE";
$query .= " ORDER BY ".$sort." ".$order.$limit;
// Otherwise, fall back to default view
} else {
$query = "SELECT ".STORY_COLUMNS." FROM ".$table." WHERE 1";
if (!$is_admin)
$query .= " AND hidden IS NOT TRUE";
$query .= " ORDER BY ".$sort." ".$order.$limit;
}
//echo $query;
if ($result = $db->execute_query($query, $params)) {
while ($row = $result->fetch_assoc()) {
$row['queue_url'] = $queue ? 1 : 0;
$results[] = $row;
}
$result->close();
} else {
db_fail();
}
return $results;
}
/*
* Return array of the POST variables we may care about
* (safer than extract($_POST))
*
* If $stripslashes is non-null, turn every value into a string:
* NULL (or an array) becomes string(""), int(0) becomes string("0").
* (It used to run stripslashes, but magic_quotes_gpc no longer exists.)
*
* If $html is non-null, run the output through htmlspecialchars,
* to translate HTML-unsafe characters. This should only be needed
* if you're outputting back to an HTML form.
*/
function get_post_data($stripslashes = NULL, $html = NULL) {
$queue = get_queue();
$results = array();
$entry['id'] = isset($_POST['id']) ? $_POST['id'] : NULL;
$entry['title'] = isset($_POST['title']) ? $_POST['title'] : NULL;
$entry['author'] = isset($_POST['author']) ? $_POST['author'] : NULL;
$entry['email'] = isset($_POST['email']) ? $_POST['email'] : NULL;
$entry['upload'] = isset($_POST['upload']) ? $_POST['upload'] : NULL;
$entry['story'] = isset($_POST['story']) ? $_POST['story'] : NULL;
$entry['comments'] = isset($_POST['comments']) ? $_POST['comments'] : NULL;
$entry['date'] = isset($_POST['date']) ? $_POST['date'] : NULL;
$entry['hidden'] = isset($_POST['hidden']) ? $_POST['hidden'] : NULL;
$entry['uploaded'] = isset($_POST['uploaded']) ? $_POST['uploaded'] : NULL;
$entry['editor'] = isset($_POST['editor']) ? $_POST['editor'] : NULL;
$entry['remove_img'] = isset($_POST['remove_img']) ? ($_POST['remove_img'] ? 1 : 0) : NULL;
$entry['queue_url'] = $queue ? 1 : 0;
if ($stripslashes) {
$entry = array_map('map_string', $entry);
}
if ($html) {
$entry = array_map('map_html', $entry);
}
$results[] = $entry;
return $results;
}
function map_string($value) {
return is_scalar($value) ? (string)$value : '';
}
function map_html($string) {
return htmlspecialchars(map_string($string), ENT_QUOTES, 'UTF-8', FALSE);
}
function unmap_html($string) {
return htmlspecialchars_decode($string, ENT_QUOTES);
}
/*
* Utility functions to sanitise data
*/
function get_id() {
global $db, $id;
if (is_numeric($id)) {
return $id;
} else if (isset($_REQUEST['id']) && is_numeric($_REQUEST['id'])) {
$id = $db->real_escape_string($_REQUEST['id']);
return $id;
} else {
return NULL;
}
}
function get_search_args() {
global $db;
if (isset($_GET['search']) && is_string($_GET['search'])) {
return $_GET['search'];
}
}
function get_order() {
global $db;
$order = NULL;
if (isset($_GET['order']) && is_string($_GET['order'])) {
$order = $db->real_escape_string($_GET['order']);
}
// Sort ascending by default
if ($order != "ASC" &&
$order != "DESC" &&
$order != "asc" &&
$order != "desc")
if (get_sort_arg() == "date") {
$order = "desc";
}
else $order = "asc";
return $order;
}
function get_sort_arg() {
global $db;
$sort = NULL;
if (isset($_GET['sort']) && is_string($_GET['sort'])) {
$sort = $db->real_escape_string($_GET['sort']);
}
if ($sort != "author" &&
$sort != "date" &&
$sort != "title")
$sort = "date";
return $sort;
}
// The most stories that one page can show
define('MAX_SHOW', 50);
// Return a whole number from the query string, limited to $min..$max,
// or NULL if the value is missing or is not a whole number
function get_int_arg($name, $min, $max = PHP_INT_MAX) {
if (!isset($_GET[$name]) || !is_string($_GET[$name]))
return NULL;
$value = filter_var(trim($_GET[$name]), FILTER_VALIDATE_INT);
if ($value === FALSE)
return NULL;
return max($min, min($max, $value));
}
function get_show_num() {
global $num_stories;
$show = get_int_arg('show', 1, MAX_SHOW);
if ($show !== NULL)
return $show;
else if ($num_stories)
return (int)$num_stories;
else // provide fallback
return 1;
}
function get_start_num() {
$start = get_int_arg('start', 0);
return $start === NULL ? 0 : $start;
}
/*
* Return the queue
*/
function get_queue() {
global $action, $queue;
if ($action == 'save' || $action == 'submit')
$queue = TRUE;
else if (isset($_REQUEST['queue']))
$queue = ($_REQUEST['queue'] === '1') ? TRUE : FALSE;
else if (!is_bool($queue))
$queue = FALSE;
return $queue;
}
/*
* Returns the table name
*/
function which_table($move = FALSE, $action = 'read') {
global $is_admin;
$queue = get_queue();
// Editing: a visitor can never write to the live table
if (!$is_admin && in_array($action, array('post', 'save', 'submit', 'delete'))) {
return 'queue';
} else if ($action == 'delete' && $move && $is_admin) {
return 'queue';
} else if ($action == 'post' && $is_admin) {
return 'xxx';
} else if ($action == 'save' || $action == 'submit') {
return 'queue';
// Viewing
} else if ($queue && $is_admin) {
return 'queue';
} else {
return 'xxx';
}
}
/*
* Uploaded images wait in a directory outside the web root until the
* story is saved. The session holds the random file name, so the client
* never sends a file path.
*/
function upload_dir() {
$dir = rtrim(config('UPLOAD_DIR') ?: sys_get_temp_dir() . '/xxx-uploads', '/');
if (!is_dir($dir))
mkdir($dir, 0700, TRUE);
return $dir;
}
/*
* A size in megabytes from the configuration, in bytes. An empty, missing
* or bad value, or one that is not above zero, gives $default_mb.
*/
function config_megabytes($key, $default_mb) {
$value = trim((string)config($key, ''));
if (!is_numeric($value) || (float)$value <= 0)
$value = $default_mb;
return (int)((float)$value * 1024 * 1024);
}
/*
* The largest image a story can have, in bytes, from UPLOAD_MAX_MB (1 MB
* by default). NULL means no limit, which UPLOAD_MAX_MB=none gives.
*/
function upload_max_bytes() {
if (strtolower(trim((string)config('UPLOAD_MAX_MB', ''))) === 'none')
return NULL;
return config_megabytes('UPLOAD_MAX_MB', 1);
}
// The most that the upload directory holds, in bytes, from UPLOAD_DIR_LIMIT_MB (100 MB by default)
function upload_dir_limit_bytes() {
return config_megabytes('UPLOAD_DIR_LIMIT_MB', 100);
}
// The same in megabytes for a message, such as "1" or "2.5"
function upload_max_mb() {
return rtrim(rtrim(number_format(upload_max_bytes() / 1048576, 2, '.', ''), '0'), '.');
}
function is_upload_name($name) {
return is_string($name) && preg_match('/^[0-9a-f]{32}\z/', $name);
}
// Upload files older than this are abandoned previews
define('UPLOAD_MAX_AGE', 86400);
/*
* Move a file from $_FILES into the upload directory, and remember it
* in the session. Returns the new path, or FALSE. A session keeps one
* upload: a new one replaces the last. Abandoned uploads are removed
* first, and nothing is kept if the directory would grow past
* UPLOAD_DIR_LIMIT_MB (100 MB by default).
*/
function store_upload($tmp_file) {
prune_uploads();
remove_pending_upload();
if (upload_dir_size() + (int)@filesize($tmp_file) > upload_dir_limit_bytes())
return FALSE;
$name = bin2hex(random_bytes(16));
$file = upload_dir() . '/' . $name;
if (!move_uploaded_file($tmp_file, $file))
return FALSE;
$_SESSION['pending_upload'] = $name;
return $file;
}
// The upload files, name => path; nothing else in the directory counts
function upload_files() {
$files = array();
foreach (glob(upload_dir() . '/*') ?: array() as $file) {
if (is_upload_name(basename($file)) && is_file($file))
$files[basename($file)] = $file;
}
return $files;
}
function upload_dir_size() {
return array_sum(array_map(function ($file) { return (int)@filesize($file); }, upload_files()));
}
// Remove the uploads that are older than UPLOAD_MAX_AGE
function prune_uploads() {
foreach (upload_files() as $file) {
if (@filemtime($file) < time() - UPLOAD_MAX_AGE)
@unlink($file);
}
}
// Remove the file of this session's upload, and forget it
function remove_pending_upload() {
if ($file = pending_upload())
@unlink($file);
unset($_SESSION['pending_upload']);
}
/*
* Return the path of the image this session uploaded earlier, or FALSE.
*/
function pending_upload() {
$name = $_SESSION['pending_upload'] ?? NULL;
if (!is_upload_name($name))
return FALSE;
$file = upload_dir() . '/' . $name;
return is_file($file) ? $file : FALSE;
}
/*
* URL that shows the pending image; the name part stops browsers from
* showing an older upload from their cache.
*/
function pending_upload_url() {
return '/display?pending=' . substr($_SESSION['pending_upload'] ?? '', 0, 8);
}