From d780bb75d42fe6e8c12f5e7fc82b50f005b37e92 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sun, 5 Jan 2020 22:18:06 +0100 Subject: [PATCH 01/16] issue: move photo icon before replies Most common parts last for more stable appearance. --- issue/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/issue/index.php b/issue/index.php index 3f665db..6aebf8e 100644 --- a/issue/index.php +++ b/issue/index.php @@ -82,15 +82,15 @@ while ($row = $query->fetch()) { showdate(array_slice(preg_split('/\D/', $row->updated), 0, 3)) ); } - if ($row->imagecount) { - print ' 📷'; - } if ($row->replycount) { printf(' %s %d', '🗨', $row->replycount ); } + if ($row->imagecount) { + print ' 📷'; + } if (isset($row->assign)) { print ' '.$row->assign.''; } -- 2.30.2 From 85d01336fad3b3110a6bdb395593e29260714acc Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sun, 5 Jan 2020 22:21:25 +0100 Subject: [PATCH 02/16] reply: accept image uploads with messages --- widget/reply.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/widget/reply.php b/widget/reply.php index 9e1c7e3..cedcc49 100644 --- a/widget/reply.php +++ b/widget/reply.php @@ -9,9 +9,19 @@ $journalcol = [ if ($_POST) { require_once 'upload.inc.php'; try { + $html = messagehtml($_POST['reply']); + if ($_FILES and isset($_FILES['image'])) { + $target = 'data/upload'; + if (!file_exists($target)) { + throw new Exception("er is geen uploadmap aanwezig op $target"); + } + $target .= '/' . $User->login; + $result = userupload($_FILES['image'], $target); + $html .= sprintf('

', $result); + } $query = $Db->set('comments', [ 'page' => $Page, - 'message' => messagehtml($_POST['reply']), + 'message' => $html, 'author' => $User->login, ]); if (!$query->rowCount()) { @@ -94,7 +104,7 @@ while ($row = $query->fetch()) { if ($User) { print '
  • '; - print '
    '; + print ''; if (isset($Issue) and $User->admin("edit $Page")) { print '

    '; printf( @@ -115,6 +125,13 @@ if ($User) { ); print "

    \n"; } + if (isset($Issue)) { + printf( + '

    ' + . '

    '."\n", + 'image', 'Beeldmateriaal', ' type="file" accept="image/*"' + ); + } printf(''."\n", 'reply', "Bericht van {$User->login}", -- 2.30.2 From 671946af5f06753cb83c9e1fdaf0bdd44ea28b8d Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sun, 5 Jan 2020 22:23:56 +0100 Subject: [PATCH 03/16] reply: accept html input from admins Forgo html formatting if text starts with an element such as

    . Restricted for safety since it's not validated. Intermediate solution to support rich contents (wysiwyg editor can be added later for accessibility). --- upload.inc.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/upload.inc.php b/upload.inc.php index afaa047..74219ed 100644 --- a/upload.inc.php +++ b/upload.inc.php @@ -41,9 +41,13 @@ function userupload($input, $target = NULL, $filename = NULL) function messagehtml($input) { # convert user textarea post to formatted html + global $User; if (empty($input)) { return; } + if ($User and $User->admin and preg_match('/\A<[a-z][^>]*>/', $input)) { + return $input; # allow html input as is if privileged + } $html = preg_replace( ["/\r?\n/", "'(?:
    \n?){2}'"], ["
    \n", "

    \n\n

    "], -- 2.30.2 From ee182f5f4ab8a688284a02e534ec555e5c3093ba Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Wed, 15 Jan 2020 14:43:39 +0100 Subject: [PATCH 04/16] reply: ignore empty image uploads Fix invalid image appended for UPLOAD_ERR_NO_FILE. --- widget/reply.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/widget/reply.php b/widget/reply.php index cedcc49..7e0ec1e 100644 --- a/widget/reply.php +++ b/widget/reply.php @@ -10,14 +10,15 @@ if ($_POST) { require_once 'upload.inc.php'; try { $html = messagehtml($_POST['reply']); - if ($_FILES and isset($_FILES['image'])) { + if ($_FILES and !empty($_FILES['image'])) { $target = 'data/upload'; if (!file_exists($target)) { throw new Exception("er is geen uploadmap aanwezig op $target"); } $target .= '/' . $User->login; - $result = userupload($_FILES['image'], $target); - $html .= sprintf('

    ', $result); + if ($result = userupload($_FILES['image'], $target)) { + $html .= sprintf('

    ', $result); + } } $query = $Db->set('comments', [ 'page' => $Page, -- 2.30.2 From 15a408fca4266bcfd04289cb2d21af5057120035 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Mon, 30 Dec 2019 07:42:11 +0100 Subject: [PATCH 05/16] page: separate method to load page contents --- article.inc.php | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/article.inc.php b/article.inc.php index 541d35e..da18368 100644 --- a/article.inc.php +++ b/article.inc.php @@ -23,20 +23,26 @@ class ArchiveArticle { $this->page = preg_replace('{^\.(?:/|$)}', '', $path); $this->link = preg_replace('{(?:/index)?\.html$}', '', $this->page); - if (file_exists($this->page)) { - $this->raw = file_get_contents($this->page); - - if (preg_match_all('{ - \G \s* - }x', $this->raw, $meta)) { - $matchlen = array_sum(array_map('strlen', $meta[0])); - $this->raw = substr($this->raw, $matchlen); # delete matched contents - $this->meta = array_combine($meta[1], $meta[2]); # [property => content] - } + $this->raw($this->page); + } - @list ($this->preface, $this->title, $this->body) = - preg_split('{

    (.*?)

    \s*}s', $this->raw, 2, PREG_SPLIT_DELIM_CAPTURE); + function raw($page) + { + if (!file_exists($page)) { + return; } + $this->raw = file_get_contents($page); + + if (preg_match_all('{ + \G \s* + }x', $this->raw, $meta)) { + $matchlen = array_sum(array_map('strlen', $meta[0])); + $this->raw = substr($this->raw, $matchlen); # delete matched contents + $this->meta = array_combine($meta[1], $meta[2]); # [property => content] + } + + @list ($this->preface, $this->title, $this->body) = + preg_split('{

    (.*?)

    \s*}s', $this->raw, 2, PREG_SPLIT_DELIM_CAPTURE); } function __get($col) -- 2.30.2 From ed38c6a76767a893a319f7bfd9229b0dad6b08db Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Tue, 4 Feb 2020 20:52:00 +0100 Subject: [PATCH 06/16] page: article method to find handler code --- article.inc.php | 24 +++++++++++++++++++++ page.php | 57 ++++++++++++++++++++----------------------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/article.inc.php b/article.inc.php index da18368..d8ddf77 100644 --- a/article.inc.php +++ b/article.inc.php @@ -50,6 +50,30 @@ class ArchiveArticle return $this->$col = $this->$col(); # run method and cache } + function handler() + { + $path = $this->link; + $this->path = ''; + $this->restricted = FALSE; + while (TRUE) { + if (file_exists("$path/.private")) { + $this->restricted = $path; + } + + if (file_exists("$path/index.php")) { + return $path; + } + + $up = strrpos($path, '/'); + $this->path = substr($path, $up) . $this->path; + $path = substr($path, 0, $up); + if ($up === FALSE) { + break; + } + } + return; + } + function safetitle() { return trim($this->meta['og:title'] ?? strip_tags($this->title)); diff --git a/page.php b/page.php index e23b3e2..f4d6057 100644 --- a/page.php +++ b/page.php @@ -122,38 +122,14 @@ $User = NULL; include_once 'auth.inc.php'; $Edit = isset($_GET['edit']); -# distinguish subpage Args from topmost Page script +# setup requested page $Args = ''; $Page = preg_replace('/\?.*/', '', @$_SERVER['PATH_INFO'] ?: $_SERVER['REQUEST_URI']); $Page = urldecode(trim($Page, '/')) ?: 'index'; -while (TRUE) { - if (file_exists("$Page/.private")) { - # access restriction - if (empty($User)) { - http_response_code(303); - $target = urlencode($_SERVER['REQUEST_URI']); - header("Location: /login?goto=$target"); - exit; - } - $PageAccess = $Page; - } - if (file_exists("$Page/index.php")) { - break; - } - - $up = strrpos($Page, '/'); - $Args = substr($Page, $up) . $Args; - $Page = substr($Page, 0, $up); - if ($up === FALSE) { - break; - } -} - -$staticpage = NULL; -if (file_exists("$Page$Args.html")) { - $staticpage = "$Page$Args.html"; +$staticpage = "$Page.html"; +if (file_exists($staticpage)) { if (is_link($staticpage)) { $target = preg_replace('/\.html$/', '', readlink($staticpage)); header("HTTP/1.1 302 Shorthand"); @@ -161,18 +137,28 @@ if (file_exists("$Page$Args.html")) { exit; } } -elseif (file_exists("$Page$Args/index.html")) { - $staticpage = "$Page$Args/index.html"; +elseif (file_exists("$Page/index.html")) { + $staticpage = "$Page/index.html"; } -elseif ($User and $User->admin("edit $Page$Args")) { - $staticpage = (file_exists("$Page/template.inc.html") ? "$Page/template.inc.html" : 'template.inc.html'); -} - -# prepare page contents require_once('article.inc.php'); $Article = new ArchiveArticle($staticpage); +$Page = $Article->handler; +$Args = $Article->path; + +if ($PageAccess = $Article->restricted) { + # access restriction + if (empty($User)) { + http_response_code(303); + $target = urlencode($Article->link); + header("Location: /login?goto=$target"); + exit; + } +} + +# prepare page contents + ob_start(); # page body $Place = [ 'user' => $User ? $User->login : '', @@ -191,6 +177,9 @@ if (isset($Article->raw)) { } $Article->raw = '
    '."\n\n".$Article->raw."
    \n\n"; } +elseif (!$Article->raw and $User and $User->admin("edit {$Article->link}")) { + $Article->raw(file_exists("$Page/template.inc.html") ? "$Page/template.inc.html" : 'template.inc.html'); +} # output dynamic and/or static html -- 2.30.2 From 8f0144192a6b624de74bb73e1a73cc16bd4b09e1 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Tue, 4 Feb 2020 21:19:29 +0100 Subject: [PATCH 07/16] login: redirection message if pending page Assume ?goto page required authorisation. --- login/form.inc.php | 7 +++++++ login/index.php | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/login/form.inc.php b/login/form.inc.php index 7e78f82..9a7c015 100644 --- a/login/form.inc.php +++ b/login/form.inc.php @@ -1,5 +1,12 @@

    Inloggen

    + +

    +De pagina title ?: $target->link ?> +is alleen toegankelijk voor leden. +

    + + handler and $target->handler == 'melding') { + $caller = $Article; + $Article = $target; + $Args = $target->path; + ob_start(); + include "./{$target->handler}/index.php"; + ob_end_clean(); + $Article = $caller; + } + if ($target->title) { $Article->title .= ' voor ' . $target->title; } -- 2.30.2 From b2f73907d35c9ea3a77c1157a08de6c73eb6e42c Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Tue, 4 Feb 2020 22:20:18 +0100 Subject: [PATCH 09/16] login: target page in description and image metadata --- login/index.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/login/index.php b/login/index.php index 0ac33ac..c69ec3c 100644 --- a/login/index.php +++ b/login/index.php @@ -54,10 +54,15 @@ if (empty($User)) { if ($target->title) { $Article->title .= ' voor ' . $target->title; } + if ($target->image) { + $Article->image = $target->image; + } } + ob_start(); require_once 'login/form.inc.php'; + $Article->raw = ob_get_clean(); $Place['warn'] = $message; - return; + return TRUE; } if (isset($_REQUEST['goto'])) { -- 2.30.2 From dcf14e6ed4093fd464367806e2211bb4bec3c8ba Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Tue, 4 Feb 2020 22:36:11 +0100 Subject: [PATCH 10/16] issue: match image replies for metadata Database paragraphs not cleaned by editor lack preceding newline. --- article.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/article.inc.php b/article.inc.php index d8ddf77..503c5a1 100644 --- a/article.inc.php +++ b/article.inc.php @@ -110,7 +110,7 @@ class ArchiveArticle function story() { if ( preg_match('{ - \n (?: < (?: p | figure [^>]* ) >\s* )+ (]*>) | \n + (?: < (?: p | figure [^>]* ) >\s* )+ (]*>) | \n }x', $this->body, $img, PREG_OFFSET_CAPTURE) ) { # strip part after matching divider (image) if (isset($img[1])) { -- 2.30.2 From 1b6e24cdaae9bf6bf6a990fe9227cb50f5d29d92 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Wed, 18 Mar 2020 17:28:59 +0100 Subject: [PATCH 11/16] page: reenclose template contents in static container Fix editing of new pages since v4.2-24-ged38c6a767 (2020-02-04) [page: article method to find handler code]. --- page.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/page.php b/page.php index f4d6057..12e0690 100644 --- a/page.php +++ b/page.php @@ -175,11 +175,13 @@ if (isset($Article->raw)) { ) . $Article->raw; } } - $Article->raw = '
    '."\n\n".$Article->raw."
    \n\n"; } -elseif (!$Article->raw and $User and $User->admin("edit {$Article->link}")) { +elseif ($User and $User->admin("edit {$Article->link}")) { $Article->raw(file_exists("$Page/template.inc.html") ? "$Page/template.inc.html" : 'template.inc.html'); } +if (isset($Article->raw)) { + $Article->raw = '
    '."\n\n".$Article->raw."
    \n\n"; +} # output dynamic and/or static html -- 2.30.2 From f6a56971c28026ca8f67783518b0ba6a39e1f8bc Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sat, 18 Jan 2020 15:14:10 +0100 Subject: [PATCH 12/16] thumb: prefer progressive jpeg encoding Intermediate rendering for faster results, and overall smaller file sizes similar (if not identical) to jpegtran -optimize or PageSpeed. --- thumb/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/thumb/index.php b/thumb/index.php index fbea220..5291047 100644 --- a/thumb/index.php +++ b/thumb/index.php @@ -90,6 +90,7 @@ function mkthumb_exec($source, $target, $width, $height) '-delete', '1--1', '-trim', '-background', 'white', '-layers', 'flatten', + '-interlace', 'plane', # progressive '-resize', "${width}x${height}", '-quality', '90%', $source, "jpg:$target" -- 2.30.2 From f0a869638caa32e6dc82f9a54bd8448015f8b638 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sat, 18 Jan 2020 15:57:10 +0100 Subject: [PATCH 13/16] thumb: decrease preferred quality to 85% Better trade-off, also recommended by PageSpeed. --- thumb/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/thumb/index.php b/thumb/index.php index 5291047..c65cab3 100644 --- a/thumb/index.php +++ b/thumb/index.php @@ -87,12 +87,12 @@ function mkthumb_exec($source, $target, $width, $height) } $cmd = implode(' ', array_map('escapeshellarg', [ 'convert', - '-delete', '1--1', + '-delete', '1--1', # static '-trim', - '-background', 'white', '-layers', 'flatten', + '-background', 'white', '-layers', 'flatten', # opaque '-interlace', 'plane', # progressive '-resize', "${width}x${height}", - '-quality', '90%', + '-quality', '85%', $source, "jpg:$target" ])); $return = shell_exec("$cmd 2>&1"); -- 2.30.2 From ead5bed24e07383fbcf783c3b5c70a755dbc1982 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sat, 18 Jan 2020 15:57:10 +0100 Subject: [PATCH 14/16] thumb: strip metadata and chroma Decreased colour quality recommended by Google PageSpeed: --- thumb/index.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/thumb/index.php b/thumb/index.php index c65cab3..d296fbf 100644 --- a/thumb/index.php +++ b/thumb/index.php @@ -91,6 +91,8 @@ function mkthumb_exec($source, $target, $width, $height) '-trim', '-background', 'white', '-layers', 'flatten', # opaque '-interlace', 'plane', # progressive + '-strip', '-taint', # omit metadata + '-sampling-factor', '4:2:0', '-colorspace', 'sRGB', # half chroma '-resize', "${width}x${height}", '-quality', '85%', $source, "jpg:$target" -- 2.30.2 From 63c023f45bfa532dcc54b292906c150c10331e9e Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sun, 17 May 2020 00:17:23 +0200 Subject: [PATCH 15/16] page: disallow frame ancestors to prevent clickjacking Security policy recommended by Dareboost, to stop potential malicious page embedding. Support should be decent (enough), so do not bother with an equivalent X-Frame-Options directive for compatibility. --- page.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/page.php b/page.php index 12e0690..3dd8cba 100644 --- a/page.php +++ b/page.php @@ -159,6 +159,8 @@ if ($PageAccess = $Article->restricted) { # prepare page contents +header("Content-Security-Policy: frame-ancestors 'none'"); + ob_start(); # page body $Place = [ 'user' => $User ? $User->login : '', -- 2.30.2 From bc04734cdf01d9b2ac8a9b9558c4782e61086821 Mon Sep 17 00:00:00 2001 From: Mischa POSLAWSKY Date: Sun, 17 May 2020 01:05:27 +0200 Subject: [PATCH 16/16] page: declare minimal security policy header Define current data usage to provide some protection from XSS attacks. Allow for remaining scripts and images (editor script, gallery, some onclick actions in user forms, inline svg) to be improved at a later time. --- page.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/page.php b/page.php index 3dd8cba..dee5ee0 100644 --- a/page.php +++ b/page.php @@ -159,7 +159,11 @@ if ($PageAccess = $Article->restricted) { # prepare page contents -header("Content-Security-Policy: frame-ancestors 'none'"); +header(sprintf('Content-Security-Policy: %s', implode('; ', [ + "default-src 'self' 'unsafe-inline' http://cdn.ckeditor.com", # some overrides remain + "img-src 'self' data: http://cdn.ckeditor.com", # inline svg (in css) + "frame-ancestors 'none'", # prevent malicious embedding +]))); ob_start(); # page body $Place = [ -- 2.30.2