318 lines
14 KiB
PHP
318 lines
14 KiB
PHP
<?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;
|
|
}
|