First git commit
This commit is contained in:
317
public_html/lib/ui.php
Normal file
317
public_html/lib/ui.php
Normal file
@ -0,0 +1,317 @@
|
||||
<?php
|
||||
// Dual-markup rendering layer: emits WML 1.1 or XHTML depending on the client.
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
// ---------------------------------------------------------------- detection
|
||||
function detect_mode(): string {
|
||||
// Explicit override wins, and is remembered in the session.
|
||||
// ?m=auto clears the override and returns to capability detection.
|
||||
$force = $_GET['m'] ?? '';
|
||||
if ($force === 'auto') {
|
||||
unset($_SESSION['mode']);
|
||||
} elseif ($force === 'wml' || $force === 'html') {
|
||||
$_SESSION['mode'] = $force;
|
||||
return $force;
|
||||
}
|
||||
if (!empty($_SESSION['mode'])) return $_SESSION['mode'];
|
||||
|
||||
$accept = strtolower($_SERVER['HTTP_ACCEPT'] ?? '');
|
||||
$ua = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||||
|
||||
// Strongest signal: the client advertises WML in Accept.
|
||||
if (strpos($accept, 'vnd.wap.wml') !== false) return 'wml';
|
||||
|
||||
// Some gateways only send x-wap-profile / wap headers.
|
||||
foreach (['HTTP_X_WAP_PROFILE','HTTP_PROFILE','HTTP_WAP_CONNECTION'] as $h) {
|
||||
if (!empty($_SERVER[$h])) return 'wml';
|
||||
}
|
||||
|
||||
// Classic WAP browser UA fingerprints.
|
||||
$wapUa = ['wap','wml','openwave','up.browser','up.link','midp','j2me','symbian',
|
||||
'nokia','ericsson','sonyerics','siemens','sagem','alcatel','panasonic',
|
||||
'philips','sanyo','sharp','lg-','lge-','samsung-','motorola','mot-',
|
||||
'blazer','avantgo','elaine','palmos','netfront','xiino','portalmmm',
|
||||
'digital paths','klondike','dolfin'];
|
||||
foreach ($wapUa as $frag) {
|
||||
if (strpos($ua, $frag) !== false) {
|
||||
// Modern smartphones match 'nokia'/'samsung' too but they always
|
||||
// advertise text/html and never vnd.wap.wml, so require the absence
|
||||
// of a real HTML accept when the UA looks modern.
|
||||
if (strpos($ua, 'android') !== false || strpos($ua, 'iphone') !== false
|
||||
|| strpos($ua, 'webkit') !== false) return 'html';
|
||||
return 'wml';
|
||||
}
|
||||
}
|
||||
return 'html';
|
||||
}
|
||||
|
||||
function mode(): string {
|
||||
static $m = null;
|
||||
if ($m === null) $m = detect_mode();
|
||||
return $m;
|
||||
}
|
||||
function is_wml(): bool { return mode() === 'wml'; }
|
||||
|
||||
// ---------------------------------------------------------------- escaping
|
||||
// WML needs $ escaped as $$ because $ introduces a variable reference.
|
||||
function e(string $s): string {
|
||||
$s = htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||
if (is_wml()) $s = str_replace('$', '$$', $s);
|
||||
return $s;
|
||||
}
|
||||
|
||||
function wtrim(string $s, int $max = WML_MAX_TEXT): string {
|
||||
if (!is_wml() || strlen($s) <= $max) return $s;
|
||||
return substr($s, 0, $max) . "\n[...truncated, view on a larger screen]";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- urls
|
||||
// WAP browsers frequently drop cookies, so the session id rides in the URL
|
||||
// whenever the client did not give us a cookie back.
|
||||
function url(string $path, array $q = []): string {
|
||||
// Sticky mode override, but never clobber an explicit m= passed by the caller
|
||||
// (the markup-toggle link depends on winning here).
|
||||
if (!empty($_GET['m']) && $_GET['m'] !== 'auto' && !isset($q['m'])) $q['m'] = $_GET['m'];
|
||||
// Only WML clients (cookies stripped by the gateway) need the session id
|
||||
// in the URL. XHTML clients and crawlers use cookies, or need no session
|
||||
// for public pages, so they get clean canonical URLs - this stops search
|
||||
// engines indexing an infinite ?WAPSID=... URL space.
|
||||
if (is_wml() && empty($_COOKIE[session_name()]) && session_id() !== '') {
|
||||
$q[session_name()] = session_id();
|
||||
}
|
||||
$u = $path;
|
||||
if ($q) $u .= (strpos($path, '?') === false ? '?' : '&') . http_build_query($q);
|
||||
return $u;
|
||||
}
|
||||
function u(string $path, array $q = []): string { return e(url($path, $q)); }
|
||||
|
||||
// ---------------------------------------------------------------- page shell
|
||||
$GLOBALS['__page_open'] = false;
|
||||
|
||||
function page_start(string $title, array $opt = []): void {
|
||||
if ($GLOBALS['__page_open']) return;
|
||||
$GLOBALS['__page_open'] = true;
|
||||
$t = e($title);
|
||||
|
||||
if (is_wml()) {
|
||||
header('Content-Type: text/vnd.wap.wml; charset=utf-8');
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
|
||||
echo '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"'
|
||||
. ' "http://www.wapforum.org/DTD/wml_1.1.xml">' . "\n";
|
||||
echo "<wml>\n<card id=\"main\" title=\"$t\">\n";
|
||||
// "Back" softkey on every deck
|
||||
if (!empty($opt['back'])) {
|
||||
echo '<do type="prev" label="Back"><go href="' . e(url($opt['back']))
|
||||
. '"/></do>' . "\n";
|
||||
}
|
||||
echo "<p><b>$t</b></p>\n";
|
||||
} else {
|
||||
header('Content-Type: application/xhtml+xml; charset=utf-8');
|
||||
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
|
||||
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
|
||||
. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
|
||||
echo '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">' . "\n";
|
||||
echo "<head>\n<title>$t - " . e(SITE_NAME) . "</title>\n";
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1"/>' . "\n";
|
||||
// Canonical URL drops the WAPSID session param and the markup switch,
|
||||
// so search engines consolidate on the clean page.
|
||||
$canon = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'wap.txt3.net')
|
||||
. (strtok($_SERVER['REQUEST_URI'] ?? '/index.php', '?'));
|
||||
echo '<link rel="canonical" href="' . e($canon) . '"/>' . "\n";
|
||||
echo '<link rel="stylesheet" href="/style.css" type="text/css"/>' . "\n";
|
||||
echo "</head>\n<body>\n<div class=\"wrap\">\n";
|
||||
echo '<h1><a href="' . u('/index.php') . '">' . e(SITE_NAME) . "</a></h1>\n";
|
||||
nav_bar();
|
||||
echo "<h2>$t</h2>\n";
|
||||
}
|
||||
}
|
||||
|
||||
function nav_bar(): void {
|
||||
if (is_wml()) return;
|
||||
$me = current_user();
|
||||
echo '<div class="nav">';
|
||||
$links = [['/index.php','Home']];
|
||||
if ($me) {
|
||||
$n = unread_count($me['id']);
|
||||
$links[] = ['/inbox.php', 'Inbox' . ($n ? " ($n)" : '')];
|
||||
$links[] = ['/forum.php', 'Forum'];
|
||||
$links[] = ['/games.php', 'Games'];
|
||||
$links[] = ['/profile.php', 'Profile'];
|
||||
$links[] = ['/about.php', 'About'];
|
||||
if ($me['is_admin']) $links[] = ['/admin/index.php', 'Admin'];
|
||||
$links[] = ['/logout.php', 'Logout'];
|
||||
} else {
|
||||
$links[] = ['/forum.php','Forum'];
|
||||
$links[] = ['/login.php','Login'];
|
||||
$links[] = ['/signup.php','Signup'];
|
||||
$links[] = ['/about.php','About'];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($links as $l) $out[] = '<a href="' . u($l[0]) . '">' . e($l[1]) . '</a>';
|
||||
echo implode(' · ', $out);
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
function page_end(): void {
|
||||
if (!$GLOBALS['__page_open']) return;
|
||||
// Always offer a link to the OTHER markup mode, in both modes, so a client
|
||||
// that switched (or was detected wrongly) can always get back.
|
||||
$other = is_wml() ? 'html' : 'wml';
|
||||
$otherLbl = is_wml() ? 'xhtml view' : 'wml view';
|
||||
// Keep the user on the page they are looking at, minus any old m= override.
|
||||
$self = strtok($_SERVER['REQUEST_URI'] ?? '/index.php', '?');
|
||||
$qs = $_GET;
|
||||
unset($qs['m'], $qs[session_name()]);
|
||||
$qs['m'] = $other;
|
||||
|
||||
if (is_wml()) {
|
||||
echo '<p><a href="' . e(url($self, $qs)) . '">' . e($otherLbl) . "</a></p>\n";
|
||||
echo "</card>\n</wml>\n";
|
||||
} else {
|
||||
echo '<div class="foot">' . e(SITE_NAME) . ' · '
|
||||
. '<a href="' . e(url($self, $qs)) . '">' . e($otherLbl) . '</a>'
|
||||
. "</div>\n</div>\n</body>\n</html>\n";
|
||||
}
|
||||
$GLOBALS['__page_open'] = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- primitives
|
||||
function p_para(string $text, string $class = ''): void {
|
||||
$t = nl2br_mode(e($text));
|
||||
if (is_wml()) echo "<p>$t</p>\n";
|
||||
else echo '<p' . ($class ? ' class="' . e($class) . '"' : '') . ">$t</p>\n";
|
||||
}
|
||||
|
||||
function nl2br_mode(string $escaped): string {
|
||||
return str_replace(["\r\n", "\n"], is_wml() ? "<br/>" : "<br />", $escaped);
|
||||
}
|
||||
|
||||
function p_err(string $t): void {
|
||||
if (is_wml()) echo '<p><b>! ' . e($t) . "</b></p>\n";
|
||||
else echo '<p class="err">' . e($t) . "</p>\n";
|
||||
}
|
||||
function p_ok(string $t): void {
|
||||
if (is_wml()) echo '<p>' . e($t) . "</p>\n";
|
||||
else echo '<p class="ok">' . e($t) . "</p>\n";
|
||||
}
|
||||
|
||||
// A vertical list of links -- one <p> per line in WML, <ul> in XHTML.
|
||||
function p_links(array $links): void {
|
||||
if (!$links) return;
|
||||
if (is_wml()) {
|
||||
foreach ($links as $l) {
|
||||
echo '<p><a href="' . e(url($l[0], $l[2] ?? [])) . '">'
|
||||
. e($l[1]) . "</a></p>\n";
|
||||
}
|
||||
} else {
|
||||
echo "<ul class=\"menu\">\n";
|
||||
foreach ($links as $l) {
|
||||
echo '<li><a href="' . e(url($l[0], $l[2] ?? [])) . '">'
|
||||
. e($l[1]) . "</a></li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
}
|
||||
}
|
||||
|
||||
function p_link(string $path, string $label, array $q = []): void {
|
||||
$a = '<a href="' . e(url($path, $q)) . '">' . e($label) . '</a>';
|
||||
echo is_wml() ? "<p>$a</p>\n" : "<p>$a</p>\n";
|
||||
}
|
||||
|
||||
function p_rule(): void { if (!is_wml()) echo "<hr />\n"; }
|
||||
|
||||
// ---------------------------------------------------------------- forms
|
||||
// $fields: list of ['name'=>, 'label'=>, 'type'=>text|password|textarea|select|hidden,
|
||||
// 'value'=>, 'options'=>[v=>label], 'maxlength'=>, 'format'=>]
|
||||
function p_form(string $action, array $fields, string $submit, array $hidden = []): void {
|
||||
$hidden['csrf'] = csrf_token();
|
||||
if (is_wml()) {
|
||||
// WML: inputs are declared in the card body, the <do> issues the POST.
|
||||
foreach ($fields as $f) {
|
||||
$type = $f['type'] ?? 'text';
|
||||
if ($type === 'hidden') { $hidden[$f['name']] = $f['value'] ?? ''; continue; }
|
||||
echo '<p>' . e($f['label']) . ':<br/>';
|
||||
if ($type === 'select') {
|
||||
echo '<select name="' . e($f['name']) . '">';
|
||||
foreach (($f['options'] ?? []) as $v => $lab) {
|
||||
echo '<option value="' . e((string)$v) . '">' . e((string)$lab) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
} else {
|
||||
// WML has no textarea; a plain input is the portable choice.
|
||||
echo '<input name="' . e($f['name']) . '"';
|
||||
if ($type === 'password') echo ' type="password"';
|
||||
if (!empty($f['maxlength'])) echo ' maxlength="' . (int)$f['maxlength'] . '"';
|
||||
if (!empty($f['format'])) echo ' format="' . e($f['format']) . '"';
|
||||
if (isset($f['value']) && $f['value'] !== '')
|
||||
echo ' value="' . e((string)$f['value']) . '"';
|
||||
echo '/>';
|
||||
}
|
||||
echo "</p>\n";
|
||||
}
|
||||
echo '<do type="accept" label="' . e($submit) . '">' . "\n";
|
||||
echo ' <go href="' . e(url($action)) . '" method="post">' . "\n";
|
||||
foreach ($hidden as $k => $v) {
|
||||
echo ' <postfield name="' . e((string)$k) . '" value="' . e((string)$v) . '"/>' . "\n";
|
||||
}
|
||||
foreach ($fields as $f) {
|
||||
if (($f['type'] ?? 'text') === 'hidden') continue;
|
||||
echo ' <postfield name="' . e($f['name']) . '" value="$(' . e($f['name']) . ')"/>' . "\n";
|
||||
}
|
||||
echo " </go>\n</do>\n";
|
||||
} else {
|
||||
echo '<form method="post" action="' . e(url($action)) . '">' . "\n<div>\n";
|
||||
foreach ($hidden as $k => $v) {
|
||||
echo '<input type="hidden" name="' . e((string)$k) . '" value="' . e((string)$v) . '"/>' . "\n";
|
||||
}
|
||||
foreach ($fields as $f) {
|
||||
$type = $f['type'] ?? 'text';
|
||||
$nm = e($f['name']);
|
||||
if ($type === 'hidden') {
|
||||
echo '<input type="hidden" name="' . $nm . '" value="' . e((string)($f['value'] ?? '')) . '"/>' . "\n";
|
||||
continue;
|
||||
}
|
||||
echo '<p><label for="f_' . $nm . '">' . e($f['label']) . "</label><br />\n";
|
||||
if ($type === 'textarea') {
|
||||
echo '<textarea id="f_' . $nm . '" name="' . $nm . '" rows="6" cols="40">'
|
||||
. e((string)($f['value'] ?? '')) . "</textarea>";
|
||||
} elseif ($type === 'select') {
|
||||
echo '<select id="f_' . $nm . '" name="' . $nm . '">';
|
||||
foreach (($f['options'] ?? []) as $v => $lab) {
|
||||
$sel = ((string)($f['value'] ?? '') === (string)$v) ? ' selected="selected"' : '';
|
||||
echo '<option value="' . e((string)$v) . '"' . $sel . '>' . e((string)$lab) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
} else {
|
||||
echo '<input id="f_' . $nm . '" type="' . ($type === 'password' ? 'password' : 'text')
|
||||
. '" name="' . $nm . '" value="' . e((string)($f['value'] ?? '')) . '"';
|
||||
if (!empty($f['maxlength'])) echo ' maxlength="' . (int)$f['maxlength'] . '"';
|
||||
echo '/>';
|
||||
}
|
||||
echo "</p>\n";
|
||||
}
|
||||
echo '<p><input type="submit" value="' . e($submit) . '"/></p>' . "\n";
|
||||
echo "</div>\n</form>\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Pager: prev/next links
|
||||
function p_pager(string $path, int $page, bool $more, array $q = []): void {
|
||||
$out = [];
|
||||
if ($page > 1) $out[] = ['/'.ltrim($path,'/'), '< Prev', $q + ['p' => $page - 1]];
|
||||
if ($more) $out[] = ['/'.ltrim($path,'/'), 'Next >', $q + ['p' => $page + 1]];
|
||||
if ($out) p_links($out);
|
||||
}
|
||||
|
||||
function bail(string $title, string $msg, string $backPath = '/index.php'): void {
|
||||
page_start($title, ['back' => $backPath]);
|
||||
p_err($msg);
|
||||
p_link($backPath, 'Back');
|
||||
page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
function redirect(string $path, array $q = []): void {
|
||||
header('Location: ' . url($path, $q));
|
||||
exit;
|
||||
}
|
||||
Reference in New Issue
Block a user