')));
- file_write($b.'/rules.txt', $_POST['rules']);
$_config = $config;
unset($config['wordfilters']);
@@ -665,7 +625,6 @@ EOT;
$query->execute() or error(db_error($query));
$board = $query->fetchAll()[0];
- $rules = @file_get_contents($board['uri'] . '/rules.txt');
$css = @file_get_contents('stylesheets/board/' . $board['uri'] . '.css');
if ($config['cache']['enabled']) {
@@ -673,5 +632,5 @@ EOT;
cache::delete('all_boards');
}
- mod_page(_('Board configuration'), 'mod/settings.html', array('board'=>$board, 'rules'=>prettify_textarea($rules), 'css'=>prettify_textarea($css), 'token'=>make_secure_link_token('settings/'.$board['uri']), 'languages'=>$possible_languages,'allowed_urls'=>$config['allowed_offsite_urls']));
+ mod_page(_('Board configuration'), 'mod/settings.html', array('board'=>$board, 'css'=>prettify_textarea($css), 'token'=>make_secure_link_token('settings/'.$board['uri']), 'languages'=>$possible_languages,'allowed_urls'=>$config['allowed_offsite_urls']));
};
diff --git a/inc/config.php b/inc/config.php
index 64b2836b..37925a3b 100644
--- a/inc/config.php
+++ b/inc/config.php
@@ -1541,6 +1541,9 @@
$config['mod']['ban_appeals'] = MOD;
// View the recent posts page
$config['mod']['recent'] = MOD;
+ // Create pages
+ $config['mod']['edit_pages'] = MOD;
+ $config['pages_max'] = 10;
// Config editor permissions
$config['mod']['config'] = array();
@@ -1731,3 +1734,6 @@
// Use CAPTCHA for reports?
$config['report_captcha'] = false;
+
+ // Allowed HTML tags in ?/edit_pages.
+ $config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr';
diff --git a/inc/functions.php b/inc/functions.php
index 4c9bb3d7..8e168507 100755
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -20,6 +20,7 @@ require_once 'inc/events.php';
require_once 'inc/api.php';
require_once 'inc/bans.php';
require_once 'inc/lib/gettext/gettext.inc';
+require_once 'inc/lib/parsedown/Parsedown.php'; // todo: option for parsedown instead of Tinyboard/STI markup
require_once 'inc/mod/auth.php';
// the user is not currently logged in as a moderator
@@ -2489,3 +2490,45 @@ function less_hostmask($hostmask) {
return implode('.', $parts);
}
+
+function prettify_textarea($s){
+ return str_replace("\t", ' ', str_replace("\n", '
', htmlentities($s)));
+}
+
+class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter {
+ public $name = 'NoExternalImages';
+ public function filter(&$uri, $c, $context) {
+ global $config;
+ $ct = $context->get('CurrentToken');
+
+ if (!$ct || $ct->name !== 'img') return true;
+
+ if (!isset($uri->host) && !isset($uri->scheme)) return true;
+
+ if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) {
+ error('No off-site links in board announcement images.');
+ }
+
+ return true;
+ }
+}
+
+function purify_html($s) {
+ global $config;
+
+ $c = HTMLPurifier_Config::createDefault();
+ $c->set('HTML.Allowed', $config['allowed_html']);
+ $uri = $c->getDefinition('URI');
+ $uri->addFilter(new HTMLPurifier_URIFilter_NoExternalImages(), $c);
+ $purifier = new HTMLPurifier($c);
+ $clean_html = $purifier->purify($s);
+ return $clean_html;
+}
+
+function markdown($s) {
+ $pd = new Parsedown();
+ $pd->setMarkupEscaped(true);
+ $pd->setimagesEnabled(false);
+
+ return $pd->text($s);
+}
diff --git a/inc/lib/parsedown b/inc/lib/parsedown
index 468d1e3d..d264130a 160000
--- a/inc/lib/parsedown
+++ b/inc/lib/parsedown
@@ -1 +1 @@
-Subproject commit 468d1e3da8bde049debee7f484a4f9c4b26b41ba
+Subproject commit d264130ab956c54d6bdf76a1b9ae18acb5ddc44f
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 1e7cb32d..5880a4fb 100644
--- a/inc/mod/pages.php
+++ b/inc/mod/pages.php
@@ -3337,6 +3337,167 @@ function mod_theme_rebuild($theme_name) {
));
}
+// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages.
+function delete_page_base($page = '', $board = false) {
+ global $config, $mod;
+
+ if (empty($board))
+ $board = false;
+
+ if (!$board && $mod['boards'][0] !== '*')
+ error($config['error']['noaccess']);
+
+ if (!hasPermission($config['mod']['edit_pages'], $board))
+ error($config['error']['noaccess']);
+
+ if ($board !== FALSE && !openBoard($board))
+ error($config['error']['noboard']);
+
+ if ($board) {
+ $query = prepare('DELETE FROM ``pages`` WHERE `board` = :board AND `name` = :name');
+ $query->bindValue(':board', ($board ? $board : NULL));
+ } else {
+ $query = prepare('DELETE FROM ``pages`` WHERE `board` IS NULL AND `name` = :name');
+ }
+ $query->bindValue(':name', $page);
+ $query->execute() or error(db_error($query));
+
+ header('Location: ?/edit_pages' . ($board ? ('/' . $board) : ''), true, $config['redirect_http']);
+}
+
+function mod_delete_page($page = '') {
+ delete_page_base($page);
+}
+
+function mod_delete_page_board($page = '', $board = false) {
+ delete_page_base($page, $board);
+}
+
+function mod_edit_page($id) {
+ global $config, $mod, $board;
+
+ $query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id');
+ $query->bindValue(':id', $id);
+ $query->execute() or error(db_error($query));
+ $page = $query->fetch();
+
+ if (!$page)
+ error(_('Could not find the page you are trying to edit.'));
+
+ if (!$page['board'] && $mod['boards'][0] !== '*')
+ error($config['error']['noaccess']);
+
+ if (!hasPermission($config['mod']['edit_pages'], $page['board']))
+ error($config['error']['noaccess']);
+
+ if ($page['board'] && !openBoard($page['board']))
+ error($config['error']['noboard']);
+
+ if (isset($_POST['method'], $_POST['content'])) {
+ $content = $_POST['content'];
+ $method = $_POST['method'];
+ $page['type'] = $method;
+
+ if (!in_array($method, array('markdown', 'html', 'infinity')))
+ error(_('Unrecognized page markup method.'));
+
+ switch ($method) {
+ case 'markdown':
+ $write = markdown($content);
+ break;
+ case 'html':
+ if (hasPermission($config['mod']['rawhtml'])) {
+ $write = $content;
+ } else {
+ $write = purify_html($content);
+ }
+ break;
+ case 'infinity':
+ $c = $content;
+ markup($content);
+ $write = $content;
+ $content = $c;
+ }
+
+ if (!isset($write) or !$write)
+ error(_('Failed to mark up your input for some reason...'));
+
+ $query = prepare('UPDATE ``pages`` SET `type` = :method, `content` = :content WHERE `id` = :id');
+ $query->bindValue(':method', $method);
+ $query->bindValue(':content', $content);
+ $query->bindValue(':id', $id);
+ $query->execute() or error(db_error($query));
+
+ $fn = ($board['uri'] ? ($board['uri'] . '/') : '') . $page['name'] . '.html';
+ $body = "
$write
";
+ $html = Element('page.html', array('config' => $config, 'body' => $body, 'title' => utf8tohtml($page['title'])));
+ file_write($fn, $html);
+ }
+
+ if (!isset($content)) {
+ $query = prepare('SELECT `content` FROM ``pages`` WHERE `id` = :id');
+ $query->bindValue(':id', $id);
+ $query->execute() or error(db_error($query));
+ $content = $query->fetchColumn();
+ }
+
+ mod_page(sprintf(_('Editing static page: %s'), $page['name']), 'mod/edit_page.html', array('page' => $page, 'token' => make_secure_link_token("edit_page/$id"), 'content' => prettify_textarea($content), 'board' => $board));
+}
+
+function mod_pages($board = false) {
+ global $config, $mod, $pdo;
+
+ if (empty($board))
+ $board = false;
+
+ if (!$board && $mod['boards'][0] !== '*')
+ error($config['error']['noaccess']);
+
+ if (!hasPermission($config['mod']['edit_pages'], $board))
+ error($config['error']['noaccess']);
+
+ if ($board !== FALSE && !openBoard($board))
+ error($config['error']['noboard']);
+
+ if ($board) {
+ $query = prepare('SELECT * FROM ``pages`` WHERE `board` = :board');
+ $query->bindValue(':board', $board);
+ } else {
+ $query = query('SELECT * FROM ``pages`` WHERE `board` IS NULL');
+ }
+ $query->execute() or error(db_error($query));
+ $pages = $query->fetchAll(PDO::FETCH_ASSOC);
+
+ if (isset($_POST['page'])) {
+ if ($board and sizeof($pages) > $config['pages_max'])
+ error(sprintf(_('Sorry, this site only allows %d pages per board.'), $config['pages_max']));
+
+ if (!preg_match('/^[a-z0-9]{1,255}$/', $_POST['page']))
+ error(_('Page names must be < 255 chars and may only contain lowercase letters A-Z and digits 1-9.'));
+
+ foreach ($pages as $i => $p) {
+ if ($_POST['page'] === $p['name'])
+ error(_('Refusing to create a new page with the same name as an existing one.'));
+ }
+
+ $title = ($_POST['title'] ? $_POST['title'] : NULL);
+
+ $query = prepare('INSERT INTO ``pages``(board, title, name) VALUES(:board, :title, :name)');
+ $query->bindValue(':board', ($board ? $board : NULL));
+ $query->bindValue(':title', $title);
+ $query->bindValue(':name', $_POST['page']);
+ $query->execute() or error(db_error($query));
+
+ $pages[] = array('id' => $pdo->lastInsertId(), 'name' => $_POST['page'], 'board' => $board, 'title' => $title);
+ }
+
+ foreach ($pages as $i => &$p) {
+ $p['delete_token'] = make_secure_link_token('edit_pages/delete/' . $p['name'] . ($board ? ('/' . $board) : ''));
+ }
+
+ mod_page(_('Pages'), 'mod/pages.html', array('pages' => $pages, 'token' => make_secure_link_token('edit_pages' . ($board ? ('/' . $board) : '')), 'board' => $board));
+}
+
function mod_debug_antispam() {
global $pdo, $config;
@@ -3459,3 +3620,4 @@ function mod_debug_apc() {
mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars));
}
+
diff --git a/install.sql b/install.sql
index 0e279283..7990c0a8 100644
--- a/install.sql
+++ b/install.sql
@@ -372,6 +372,23 @@ CREATE TABLE `filters` (
UNIQUE KEY `data` (`type`,`value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `pages`
+--
+
+CREATE TABLE `pages` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `board` varchar(255) DEFAULT NULL,
+ `name` varchar(255) NOT NULL,
+ `title` varchar(255) DEFAULT NULL,
+ `type` varchar(255) DEFAULT NULL,
+ `content` text,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `u_pages` (`name`,`board`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
diff --git a/mod.php b/mod.php
index bc9d6057..4a5d18c6 100644
--- a/mod.php
+++ b/mod.php
@@ -35,6 +35,11 @@ $pages = array(
'/edit_news' => 'secure_POST news', // view news
'/edit_news/(\d+)' => 'secure_POST news', // view news
'/edit_news/delete/(\d+)' => 'secure news_delete', // delete from news
+
+ '/edit_pages(?:/?(\%b)?)' => 'secure_POST pages',
+ '/edit_page/(\d+)' => 'secure_POST edit_page',
+ '/edit_pages/delete/([a-z0-9]+)' => 'secure delete_page',
+ '/edit_pages/delete/([a-z0-9]+)/(\%b)' => 'secure delete_page_board',
'/noticeboard' => 'secure_POST noticeboard', // view noticeboard
'/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard
diff --git a/templates/mod/edit_page.html b/templates/mod/edit_page.html
new file mode 100644
index 00000000..3d132767
--- /dev/null
+++ b/templates/mod/edit_page.html
@@ -0,0 +1,29 @@
+
+
+
diff --git a/templates/mod/pages.html b/templates/mod/pages.html
new file mode 100644
index 00000000..c2395c02
--- /dev/null
+++ b/templates/mod/pages.html
@@ -0,0 +1,34 @@
+
+
+
+{% if board %}
+{% set page_max = config.pages_max %}
+{% trans %}This page allows you to create static pages for your board. The limit is {{ page_max }} pages per board. You will still have to link to your pages somewhere in your board, for example in a sticky or in the board's announcement. To make links in the board's announcement, use <a> HTML tags.{% endtrans %}
+{% else %}
+{% trans %}This page allows you to create static pages for your imageboard.{% endtrans %}
+{% endif %}
+
{% trans %}Rules{% endtrans %} {% trans %}Allowed tags:{% endtrans %} p li ol ul strong em u h2 {% trans %}Rules will appear at:{% endtrans %} https://8ch.net/{{board.uri}}/rules.html
{% trans %}Stylesheet{% endtrans %} {% trans %}note: does not validate CSS{% endtrans %} {% trans %}Allowed URLs:{% endtrans %} {{ allowed_urls|join(' ') }}