Take the site address from SITE_URL.

The feed links, the notification email, the webmaster address and the cookie domain now use it.
This commit is contained in:
tim
2026-09-26 15:27:51 +02:00
parent 52f4855850
commit 5d657d7e45
11 changed files with 75 additions and 30 deletions
+4 -2
View File
@@ -7,8 +7,10 @@ DB_USER=xxx
DB_PASS=
DB_NAME=xxx
# Address of the site. The pages send plain HTTP requests to this host
# over HTTPS. If it is empty, the pages refuse plain HTTP.
# Address of the site. The RSS feed links, the notification email and the
# cookie domain use it, and the pages send plain HTTP requests to this
# host over HTTPS. The email addresses are editor@ and webmaster@ this
# host. If it is empty, the pages refuse plain HTTP.
SITE_URL=https://xxx.org.za
# Google reCAPTCHA v2 keys (https://www.google.com/recaptcha/admin).
+1 -1
View File
@@ -75,7 +75,7 @@ the centre <span class="xxx">x</span> a multiplication sign. Simply put, ten by
</p>
<h2>Something is broken</h2>
<p>
If you find anything which seems to be broken, please <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#119;&#101;&#98;&#109;&#97;&#115;&#116;&#101;&#114;&#64;&#120;&#120;&#120;&#46;&#111;&#114;&#103;&#46;&#122;&#97;">email us</a>
If you find anything which seems to be broken, please <a href="<?= hidden_email('mailto:' . site_email('webmaster')) ?>">email us</a>
and tell us.
</p>
+36 -6
View File
@@ -41,20 +41,50 @@ function config($key, $default = NULL) {
}
/*
* Send a plain HTTP request to the same page over HTTPS, and stop. The
* host comes from SITE_URL, because the browser controls the Host header.
* The address of the site comes from SITE_URL, never from the Host
* header, which the browser controls. site_url() gives the scheme, host
* and port (for example "https://xxx.org.za"), and site_host() gives the
* host. Both are empty if SITE_URL has no host.
*/
function site_url_parts() {
$site = parse_url(trim((string)config('SITE_URL', '')));
return is_array($site) && !empty($site['host']) ? $site : array();
}
function site_host() {
return site_url_parts()['host'] ?? '';
}
function site_url($scheme = NULL) {
$site = site_url_parts();
if (!$site)
return '';
$port = isset($site['port']) ? ':' . $site['port'] : '';
return ($scheme ?? $site['scheme'] ?? 'https') . '://' . $site['host'] . $port;
}
// An address at the site's host, such as "editor@xxx.org.za"
function site_email($name) {
return $name . '@' . site_host();
}
// The cookie domain: the site's host and its subdomains
function cookie_domain() {
return site_host() === '' ? '' : '.' . site_host();
}
/*
* Send a plain HTTP request to the same page over HTTPS, and stop.
* Without SITE_URL, the page refuses plain HTTP.
*/
function require_https() {
if (isset($_SERVER['HTTPS']))
return;
$site = parse_url((string)config('SITE_URL', ''));
if (empty($site['host'])) {
if (site_url() === '') {
http_response_code(403);
echo "This page needs HTTPS.\n";
} else {
$port = isset($site['port']) ? ':' . $site['port'] : '';
header('Location: https://' . $site['host'] . $port . $_SERVER['REQUEST_URI']);
header('Location: ' . site_url('https') . $_SERVER['REQUEST_URI']);
}
exit;
}
+1 -1
View File
@@ -5,7 +5,7 @@ require_once('./get_data.lib.php');
$queue = get_queue();
if ($queue) session_cache_limiter('nocache');
else session_cache_limiter('none');
session_set_cookie_params(array('lifetime' => 0, 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
session_set_cookie_params(array('lifetime' => 0, 'path' => '/', 'domain' => cookie_domain(), 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
session_start();
if (isset($_SESSION['editor'])) {
$is_admin = TRUE;
+11 -9
View File
@@ -10,6 +10,8 @@ $show = get_show_num();
$table = "xxx";
header("Content-Type: application/rss+xml");
$site = xml_text(site_url());
// Set up a RSS "header"
$query = "SELECT MAX(`date`) FROM $table;";
@@ -21,19 +23,19 @@ echo '<?xml version="1.0"?>'."\n";
echo '<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">'."\n";
echo '<channel>'."\n";
echo '<title>x.x.x</title>'."\n";
echo '<link>http://xxx.org.za/</link>'."\n";
echo '<description>Most recent articles from xxx.org.za</description>'."\n";
echo '<link>'.$site.'/</link>'."\n";
echo '<description>Most recent articles from '.xml_text(site_host()).'</description>'."\n";
echo '<language>en-za</language>'."\n";
echo '<pubDate>'.$now.'</pubDate>'."\n";
echo '<lastBuildDate>'.date("D, d M Y H:i:s O", strtotime($most_recent[0])).'</lastBuildDate>'."\n";
echo '<managingEditor>&#101;&#100;&#105;&#116;&#111;&#114;&#64;&#120;&#120;&#120;&#46;&#111;&#114;&#103;&#46;&#122;&#97; (Editors)</managingEditor>'."\n";
echo '<webMaster>&#119;&#101;&#98;&#109;&#97;&#115;&#116;&#101;&#114;&#64;&#120;&#120;&#120;&#46;&#111;&#114;&#103;&#46;&#122;&#97; (Webmasters)</webMaster>'."\n";
echo '<atom:link href="http://xxx.org.za/feed" rel="self" type="application/rss+xml" />'."\n";
echo '<managingEditor>'.hidden_email(site_email('editor')).' (Editors)</managingEditor>'."\n";
echo '<webMaster>'.hidden_email(site_email('webmaster')).' (Webmasters)</webMaster>'."\n";
echo '<atom:link href="'.$site.'/feed" rel="self" type="application/rss+xml" />'."\n";
echo '<item>'."\n";
echo '<title>Front Page</title>'."\n";
echo '<link>http://xxx.org.za/</link>'."\n";
echo '<link>'.$site.'/</link>'."\n";
echo '<pubDate>'.date("D, d M Y H:i:s O", strtotime($most_recent[0])).'</pubDate>'."\n";
echo '<guid>http://xxx.org.za/</guid>'."\n";
echo '<guid>'.$site.'/</guid>'."\n";
echo '</item>'."\n";
/*
@@ -52,14 +54,14 @@ foreach ($result as $entry) {
$id = (int)$entry['id'];
echo '<item>'."\n";
echo '<title>'.xml_text($entry['title']).'</title>'."\n";
echo '<link>http://xxx.org.za/article?id='.$id.'</link>'."\n";
echo '<link>'.$site.'/article?id='.$id.'</link>'."\n";
echo '<pubDate>'.date("D, d M Y H:i:s O", strtotime($entry['date'])).'</pubDate>'."\n";
if ($entry['img'])
echo '<description>Image: '.xml_text($entry['title']).'</description>'."\n";
else
echo '<description>'.xml_text(strip_tags($entry['story'])).'</description>'."\n";
echo '<dc:creator>'.xml_text($entry['author']).'</dc:creator>'."\n";
echo '<guid>http://xxx.org.za/article?id='.$id.'</guid>'."\n";
echo '<guid>'.$site.'/article?id='.$id.'</guid>'."\n";
echo '</item>'."\n";
}
+5
View File
@@ -8,4 +8,9 @@ function h($string) {
function form_value($string) {
return htmlspecialchars((string)$string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// Text as numeric character references, which hides an email address from simple harvesters
function hidden_email($address) {
return preg_replace_callback('/./s', function ($char) { return '&#' . ord($char[0]) . ';'; }, (string)$address);
}
?>
+3 -3
View File
@@ -261,9 +261,9 @@ function sanitise($story) {
function send_email($title, $author, $story) {
include_once('../lib/convert_entities.inc.php');
$to = 'editor@xxx.org.za';
$to = site_email('editor');
$subject = "New submission: $title";
$headers = 'From: x.x.x Queue <editor@xxx.org.za>'."\r\n";
$headers = 'From: x.x.x Queue <'.site_email('editor').'>'."\r\n";
$message = "Please review the queue. $author has sent the following new submission:\n\n";
if ($story) {
// Turn line and paragraph breaks into new lines before the tags are removed,
@@ -275,6 +275,6 @@ function send_email($title, $author, $story) {
}
else
$message .= "<Submission is an image. Review on the site.>";
$message .= "\n\nhttp://xxx.org.za/publish/edit";
$message .= "\n\n" . site_url() . "/publish/edit";
mb_send_mail($to, $subject, $message, $headers);
}
+3 -3
View File
@@ -7,7 +7,7 @@ $is_admin = FALSE;
if (isset($_SERVER['HTTPS'])) {
session_cache_limiter('private');
session_set_cookie_params(array('lifetime' => 0, 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
session_set_cookie_params(array('lifetime' => 0, 'path' => '/', 'domain' => cookie_domain(), 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
session_start();
if (isset($_SESSION['editor'])) {
@@ -83,7 +83,7 @@ function create_persistent_login($username) {
open_db('close');
// Create cookie with key
setcookie('PersistentAuth', "$username|$key", array('expires' => $validity, 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
setcookie('PersistentAuth', "$username|$key", array('expires' => $validity, 'path' => '/', 'domain' => cookie_domain(), 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
return true;
}
@@ -110,7 +110,7 @@ function delete_persistent_login() {
open_db('close');
// Delete cookie
setcookie("PersistentAuth", '', array('expires' => strtotime('-1 year'), 'path' => '/', 'domain' => '.xxx.org.za', 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
setcookie("PersistentAuth", '', array('expires' => strtotime('-1 year'), 'path' => '/', 'domain' => cookie_domain(), 'secure' => TRUE, 'httponly' => TRUE, 'samesite' => 'Lax'));
return true;
}
+5 -1
View File
@@ -31,7 +31,11 @@ class FeedAndImageTest extends PageTestCase {
// The front page, then the visible stories, newest first
$this->assertSame(array('Front Page', 'Tom & "Jerry"', 'Picture', 'Not an image'),
array_map('strval', iterator_to_array($xml->xpath('/rss/channel/item/title'), FALSE)));
$this->assertSame('http://xxx.org.za/article?id=1', (string)$items[1]->link);
// The addresses come from SITE_URL
$this->assertSame('https://site.test/', (string)$xml->channel->link);
$this->assertSame('https://site.test/article?id=1', (string)$items[1]->link);
$this->assertSame('editor@site.test (Editors)', (string)$xml->channel->managingEditor);
$this->assertStringContainsString('&#101;&#100;&#105;', $feed['body']);
$this->assertSame('Wait… a bold move & more.', (string)$items[1]->description);
$this->assertSame('Ann <ann>', (string)$items[1]->children('http://purl.org/dc/elements/1.1/')->creator);
$this->assertSame('Image: Picture', (string)$items[2]->description);
+3 -1
View File
@@ -32,7 +32,7 @@ class FrontPagesTest extends PageTestCase {
public function testTheSessionCookieIsSecure(): void {
$cookie = $this->header($this->get('/'), 'Set-Cookie');
$this->assertMatchesRegularExpression('/^PHPSESSID=[0-9a-z,-]+;/', $cookie);
foreach (array('path=/', 'domain=.xxx.org.za', 'secure', 'HttpOnly', 'SameSite=Lax') as $flag)
foreach (array('path=/', 'domain=.site.test', 'secure', 'HttpOnly', 'SameSite=Lax') as $flag)
$this->assertStringContainsString("; $flag", $cookie);
}
@@ -89,6 +89,8 @@ class FrontPagesTest extends PageTestCase {
$page = $this->get('/about');
$this->assertSame(200, $page['status']);
$this->assertStringContainsString('</html>', $page['body']);
// The webmaster's address, at the host of SITE_URL, hidden from harvesters
$this->assertStringContainsString('<a href="' . hidden_email('mailto:webmaster@site.test') . '">email us</a>', $page['body']);
}
public function testTheTestsAreNotServed(): void {
+3 -3
View File
@@ -16,8 +16,8 @@ class EmailTest extends TestCase {
public function testGoesToTheEditorsFromTheQueue(): void {
$mail = $this->send('A title', 'Ann', '<p>Hello.</p>');
$this->assertSame('editor@xxx.org.za', $mail['headers']['to']);
$this->assertSame('x.x.x Queue <editor@xxx.org.za>', $mail['headers']['from']);
$this->assertSame('editor@site.test', $mail['headers']['to']);
$this->assertSame('x.x.x Queue <editor@site.test>', $mail['headers']['from']);
$this->assertSame('New submission: A title', $mail['subject']);
}
@@ -29,7 +29,7 @@ class EmailTest extends TestCase {
$mail = $this->send('T', 'Ann', "<p>First &amp; <strong>bold</strong>.</p>\n<p>Line one<br />line two</p><p></p><p>&lt;b&gt; is text &#8220;quoted&#8221;</p>");
$this->assertSame("Please review the queue. Ann has sent the following new submission:\n\n"
. "First & bold.\n\nLine one\nline two\n\n<b> is text “quoted”"
. "\n\nhttp://xxx.org.za/publish/edit", trim($mail['body']));
. "\n\nhttps://site.test/publish/edit", trim($mail['body']));
}
public function testLongLinesAreWrapped(): void {