@@ -1,3 +1,9 @@ | |||||
[submodule "js/wPaint"] | [submodule "js/wPaint"] | ||||
path = js/wPaint | path = js/wPaint | ||||
url = https://github.com/vichan-devel/wPaint.git | url = https://github.com/vichan-devel/wPaint.git | ||||
branch = master | |||||
[submodule "inc/lib/parsedown"] | |||||
path = inc/lib/parsedown | |||||
url = https://github.com/vichan-devel/parsedown | |||||
branch = master |
@@ -1498,6 +1498,9 @@ | |||||
$config['mod']['ban_appeals'] = MOD; | $config['mod']['ban_appeals'] = MOD; | ||||
// View the recent posts page | // View the recent posts page | ||||
$config['mod']['recent'] = MOD; | $config['mod']['recent'] = MOD; | ||||
// Create pages | |||||
$config['mod']['edit_pages'] = MOD; | |||||
$config['pages_max'] = 10; | |||||
// Config editor permissions | // Config editor permissions | ||||
$config['mod']['config'] = array(); | $config['mod']['config'] = array(); | ||||
@@ -1702,3 +1705,6 @@ | |||||
// Use CAPTCHA for reports? | // Use CAPTCHA for reports? | ||||
$config['report_captcha'] = false; | $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'; |
@@ -20,6 +20,7 @@ require_once 'inc/events.php'; | |||||
require_once 'inc/api.php'; | require_once 'inc/api.php'; | ||||
require_once 'inc/mod/auth.php'; | require_once 'inc/mod/auth.php'; | ||||
require_once 'inc/polyfill.php'; | require_once 'inc/polyfill.php'; | ||||
//require_once 'inc/lib/parsedown/Parsedown.php'; // we don't need that right now, do we? | |||||
if (!extension_loaded('gettext')) { | if (!extension_loaded('gettext')) { | ||||
require_once 'inc/lib/gettext/gettext.inc'; | require_once 'inc/lib/gettext/gettext.inc'; | ||||
@@ -2739,3 +2740,45 @@ function link_for($post, $page50 = false, $foreignlink = false, $thread = false) | |||||
return sprintf($tpl, $id, $slug); | return sprintf($tpl, $id, $slug); | ||||
} | } | ||||
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); | |||||
} |
@@ -2628,6 +2628,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 = "<div class='ban'>$write</div>"; | |||||
$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() { | function mod_debug_antispam() { | ||||
global $pdo, $config; | global $pdo, $config; | ||||
@@ -2744,3 +2905,4 @@ function mod_debug_apc() { | |||||
mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars)); | mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars)); | ||||
} | } | ||||
@@ -245,7 +245,7 @@ CREATE TABLE IF NOT EXISTS `search_queries` ( | |||||
`ip` varchar(39) NOT NULL, | `ip` varchar(39) NOT NULL, | ||||
`time` int(11) NOT NULL, | `time` int(11) NOT NULL, | ||||
`query` text NOT NULL | `query` text NOT NULL | ||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; | |||||
-- -------------------------------------------------------- | -- -------------------------------------------------------- | ||||
@@ -297,6 +297,24 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` ( | |||||
KEY `ban_id` (`ban_id`) | KEY `ban_id` (`ban_id`) | ||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | ||||
-- -------------------------------------------------------- | |||||
-- | |||||
-- 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=MyISAM DEFAULT CHARSET=utf8mb4; | |||||
>>>>>>> 12fa8ec... Edit static pages commit | |||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | ||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; | ||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; |
@@ -33,11 +33,19 @@ $pages = array( | |||||
'/log' => 'log', // modlog | '/log' => 'log', // modlog | ||||
'/log/(\d+)' => 'log', // modlog | '/log/(\d+)' => 'log', // modlog | ||||
'/log:([^/]+)' => 'user_log', // modlog | |||||
'/log:([^/]+)/(\d+)' => 'user_log', // modlog | |||||
'/news' => 'secure_POST news', // view news | |||||
'/news/(\d+)' => 'secure_POST news', // view news | |||||
'/news/delete/(\d+)' => 'secure news_delete', // delete from news | |||||
'/log:([^/:]+)' => 'user_log', // modlog | |||||
'/log:([^/:]+)/(\d+)' => 'user_log', // modlog | |||||
'/log:b:([^/]+)' => 'board_log', // modlog | |||||
'/log:b:([^/]+)/(\d+)' => 'board_log', // modlog | |||||
'/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' => 'secure_POST noticeboard', // view noticeboard | ||||
'/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard | '/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard | ||||
@@ -0,0 +1,29 @@ | |||||
<div style="text-align:center"> | |||||
<form method="POST"> | |||||
<input name="token" value="{{ token }}" type="hidden"> | |||||
<table> | |||||
<tr> | |||||
<th>{% trans %}Markup method{% endtrans %} | |||||
{% set allowed_html = config.allowed_html %} | |||||
{% trans %}<p class="unimportant">"markdown" is provided by <a href="http://parsedown.org/">parsedown</a>. Note: images disabled.</p> | |||||
<p class="unimportant">"html" allows the following tags:<br/>{{ allowed_html }}</p> | |||||
<p class="unimportant">"infinity" is the same as what is used in posts.</p> | |||||
<p class="unimportant">This page will not convert between formats,<br/>choose it once or do the conversion yourself!</p>{% endtrans %} | |||||
</th> | |||||
<td> | |||||
<select name="method"> | |||||
{% for markup in ['markdown', 'html', 'infinity'] %} | |||||
<option value="{{ markup }}" {% if page.type == markup %}selected{% endif %}>{{ markup }}</option> | |||||
{% endfor %} | |||||
</select> | |||||
</td></tr> | |||||
<tr><th>{% trans %}Page content{% endtrans %} | |||||
<br/> | |||||
<span class="unimportant">{% trans %}Page will appear at:{% endtrans %} | |||||
{% if board %} <a href="/{{ board.uri }}/{{ page.name }}.html">{{ config.domain }}/{{ board.uri }}/{{ page.name }}.html</a> | |||||
{% else %} <a href="/{{ page.name }}.html">{{ config.site }}/{{ page.name }}.html</a> | |||||
{% endif %}</span></th><td><textarea name="content" style="height:500px;width:500px">{{content}}</textarea></td><tr> | |||||
</table> | |||||
<input type="submit" value="Save page"> | |||||
</form> | |||||
</div> |
@@ -0,0 +1,34 @@ | |||||
<script type="text/javascript" src="js/jquery.min.js"></script> | |||||
<div style="text-align:center"> | |||||
<p class="unimportant"> | |||||
{% 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 %} | |||||
<h2>{% trans %}Existing pages{% endtrans %}</h2> | |||||
{% if pages %} | |||||
<form> | |||||
<table style="margin:auto"> | |||||
<tr><th>{% trans %}URL{% endtrans %}</th><th>{% trans %}Title{% endtrans %}</th><th>{% trans %}Edit{% endtrans %}</th><th>{% trans %}Delete{% endtrans %}</tr> | |||||
{% for page in pages %} | |||||
<tr><td>{{ page.name }}</td><td>{{ page.title }}</td><td><a href="?/edit_page/{{ page.id }}">{% trans %}Edit{% endtrans %}</a></td><td><a href="?/edit_pages/delete/{{ page.name }}{% if board %}/{{ board }}{% endif %}/{{ page.delete_token }}">{% trans %}Delete{% endtrans %}</a></td> | |||||
{% endfor %} | |||||
{% else %} | |||||
<em>No pages yet!</em> | |||||
{% endif %} | |||||
</table> | |||||
</form> | |||||
<hr/> | |||||
<h2>{% trans %}Create a new page{% endtrans %}</h2> | |||||
<form method="POST"> | |||||
<input type="hidden" name="token" value="{{ token }}"> | |||||
<table> | |||||
<tr><th>{% trans %}URL{% endtrans %}</th><th>{% trans %}Title{% endtrans %}</th></tr> | |||||
<tr><td><input type="text" name="page"></td><td><input type="text" name="title"></td> | |||||
</table> | |||||
<input type="submit" value="{% trans %}Create{% endtrans %}"> | |||||
</form> | |||||
</div> |
@@ -0,0 +1,16 @@ | |||||
<?php | |||||
// This script imports rules.txt files from the old system into the new ``pages`` table. | |||||
require dirname(__FILE__) . '/inc/cli.php'; | |||||
$boards = listBoards(TRUE); | |||||
foreach ($boards as $i => $b) { | |||||
$rules = @file_get_contents($b.'/rules.txt'); | |||||
if ($rules && !empty(trim($rules))) { | |||||
$query = prepare('INSERT INTO ``pages``(name, title, type, board, content) VALUES("rules", "Rules", "html", :board, :content)'); | |||||
$query->bindValue(':board', $b); | |||||
$query->bindValue(':content', $rules); | |||||
$query->execute() or error(db_error($query)); | |||||
} | |||||
} |