· 9 years ago · Mar 26, 2017, 02:18 PM
1<?php
2
3/*
4 * Copyright (c) 2010-2013 Tinyboard Development Group
5 */
6
7defined('TINYBOARD') or exit;
8
9function mod_page($title, $template, $args, $subtitle = false) {
10 global $config, $mod;
11
12 echo Element('page.html', array(
13 'config' => $config,
14 'mod' => $mod,
15 'hide_dashboard_link' => $template == 'mod/dashboard.html',
16 'title' => $title,
17 'subtitle' => $subtitle,
18 'nojavascript' => true,
19 'body' => Element($template,
20 array_merge(
21 array('config' => $config, 'mod' => $mod),
22 $args
23 )
24 )
25 )
26 );
27}
28
29function mod_login($redirect = false) {
30 global $config;
31
32 $args = array();
33
34 if (isset($_POST['login'])) {
35 // Check if inputs are set and not empty
36 if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') {
37 $args['error'] = $config['error']['invalid'];
38 } elseif (!login($_POST['username'], $_POST['password'])) {
39 if ($config['syslog'])
40 _syslog(LOG_WARNING, 'Unauthorized login attempt!');
41
42 $args['error'] = $config['error']['invalid'];
43 } else {
44 modLog('Logged in');
45
46 // Login successful
47 // Set cookies
48 setCookies();
49
50 if ($redirect)
51 header('Location: ?' . $redirect, true, $config['redirect_http']);
52 else
53 header('Location: ?/', true, $config['redirect_http']);
54 }
55 }
56
57 if (isset($_POST['username']))
58 $args['username'] = $_POST['username'];
59
60 mod_page(_('Login'), 'mod/login.html', $args);
61}
62
63function mod_confirm($request) {
64 mod_page(_('Confirm action'), 'mod/confirm.html', array('request' => $request, 'token' => make_secure_link_token($request)));
65}
66
67function mod_logout() {
68 global $config;
69 destroyCookies();
70
71 header('Location: ?/', true, $config['redirect_http']);
72}
73
74function mod_dashboard() {
75 global $config, $mod;
76
77 $args = array();
78
79 $args['boards'] = listBoards();
80
81 if (hasPermission($config['mod']['noticeboard'])) {
82 if (!$config['cache']['enabled'] || !$args['noticeboard'] = cache::get('noticeboard_preview')) {
83 $query = prepare("SELECT ``noticeboard``.*, `username` FROM ``noticeboard`` LEFT JOIN ``mods`` ON ``mods``.`id` = `mod` ORDER BY `id` DESC LIMIT :limit");
84 $query->bindValue(':limit', $config['mod']['noticeboard_dashboard'], PDO::PARAM_INT);
85 $query->execute() or error(db_error($query));
86 $args['noticeboard'] = $query->fetchAll(PDO::FETCH_ASSOC);
87
88 if ($config['cache']['enabled'])
89 cache::set('noticeboard_preview', $args['noticeboard']);
90 }
91 }
92
93 if (!$config['cache']['enabled'] || ($args['unread_pms'] = cache::get('pm_unreadcount_' . $mod['id'])) === false) {
94 $query = prepare('SELECT COUNT(*) FROM ``pms`` WHERE `to` = :id AND `unread` = 1');
95 $query->bindValue(':id', $mod['id']);
96 $query->execute() or error(db_error($query));
97 $args['unread_pms'] = $query->fetchColumn();
98
99 if ($config['cache']['enabled'])
100 cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
101 }
102
103 $query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
104 $args['reports'] = $query->fetchColumn();
105
106 if ($mod['type'] >= ADMIN && $config['check_updates']) {
107 if (!$config['version'])
108 error(_('Could not find current version! (Check .installed)'));
109
110 if (isset($_COOKIE['update'])) {
111 $latest = unserialize($_COOKIE['update']);
112 } else {
113 $ctx = stream_context_create(array('http' => array('timeout' => 5)));
114 if ($code = @file_get_contents('http://tinyboard.org/version.txt', 0, $ctx)) {
115 $ver = strtok($code, "\n");
116
117 if (preg_match('@^// v(\d+)\.(\d+)\.(\d+)\s*?$@', $ver, $matches)) {
118 $latest = array(
119 'massive' => $matches[1],
120 'major' => $matches[2],
121 'minor' => $matches[3]
122 );
123 if (preg_match('/v(\d+)\.(\d)\.(\d+)(-dev.+)?$/', $config['version'], $matches)) {
124 $current = array(
125 'massive' => (int) $matches[1],
126 'major' => (int) $matches[2],
127 'minor' => (int) $matches[3]
128 );
129 if (isset($m[4])) {
130 // Development versions are always ahead in the versioning numbers
131 $current['minor'] --;
132 }
133 // Check if it's newer
134 if (!( $latest['massive'] > $current['massive'] ||
135 $latest['major'] > $current['major'] ||
136 ($latest['massive'] == $current['massive'] &&
137 $latest['major'] == $current['major'] &&
138 $latest['minor'] > $current['minor']
139 )))
140 $latest = false;
141 } else {
142 $latest = false;
143 }
144 } else {
145 // Couldn't get latest version
146 $latest = false;
147 }
148 } else {
149 // Couldn't get latest version
150 $latest = false;
151 }
152
153 setcookie('update', serialize($latest), time() + $config['check_updates_time'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
154 }
155
156 if ($latest)
157 $args['newer_release'] = $latest;
158 }
159
160 $args['logout_token'] = make_secure_link_token('logout');
161
162 mod_page(_('Dashboard'), 'mod/dashboard.html', $args);
163}
164
165function mod_search_redirect() {
166 global $config;
167
168 if (!hasPermission($config['mod']['search']))
169 error($config['error']['noaccess']);
170
171 if (isset($_POST['query'], $_POST['type']) && in_array($_POST['type'], array('posts', 'IP_notes', 'bans', 'log'))) {
172 $query = $_POST['query'];
173 $query = urlencode($query);
174 $query = str_replace('_', '%5F', $query);
175 $query = str_replace('+', '_', $query);
176
177 if ($query === '') {
178 header('Location: ?/', true, $config['redirect_http']);
179 return;
180 }
181
182 header('Location: ?/search/' . $_POST['type'] . '/' . $query, true, $config['redirect_http']);
183 } else {
184 header('Location: ?/', true, $config['redirect_http']);
185 }
186}
187
188function mod_search($type, $search_query_escaped, $page_no = 1) {
189 global $pdo, $config;
190
191 if (!hasPermission($config['mod']['search']))
192 error($config['error']['noaccess']);
193
194 // Unescape query
195 $query = str_replace('_', ' ', $search_query_escaped);
196 $query = urldecode($query);
197 $search_query = $query;
198
199 // Form a series of LIKE clauses for the query.
200 // This gets a little complicated.
201
202 // Escape "escape" character
203 $query = str_replace('!', '!!', $query);
204
205 // Escape SQL wildcard
206 $query = str_replace('%', '!%', $query);
207
208 // Use asterisk as wildcard instead
209 $query = str_replace('*', '%', $query);
210
211 $query = str_replace('`', '!`', $query);
212
213 // Array of phrases to match
214 $match = array();
215
216 // Exact phrases ("like this")
217 if (preg_match_all('/"(.+?)"/', $query, $exact_phrases)) {
218 $exact_phrases = $exact_phrases[1];
219 foreach ($exact_phrases as $phrase) {
220 $query = str_replace("\"{$phrase}\"", '', $query);
221 $match[] = $pdo->quote($phrase);
222 }
223 }
224
225 // Non-exact phrases (ie. plain keywords)
226 $keywords = explode(' ', $query);
227 foreach ($keywords as $word) {
228 if (empty($word))
229 continue;
230 $match[] = $pdo->quote($word);
231 }
232
233 // Which `field` to search?
234 if ($type == 'posts')
235 $sql_field = array('body_nomarkup', 'files', 'subject', 'filehash', 'ip', 'name', 'trip');
236 if ($type == 'IP_notes')
237 $sql_field = 'body';
238 if ($type == 'bans')
239 $sql_field = 'reason';
240 if ($type == 'log')
241 $sql_field = 'text';
242
243 // Build the "LIKE 'this' AND LIKE 'that'" etc. part of the SQL query
244 $sql_like = '';
245 foreach ($match as $phrase) {
246 if (!empty($sql_like))
247 $sql_like .= ' AND ';
248 $phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase);
249 if (is_array($sql_field)) {
250 foreach ($sql_field as $field) {
251 $sql_like .= '`' . $field . '` LIKE ' . $phrase . ' ESCAPE \'!\' OR';
252 }
253 $sql_like = preg_replace('/ OR$/', '', $sql_like);
254 } else {
255 $sql_like .= '`' . $sql_field . '` LIKE ' . $phrase . ' ESCAPE \'!\'';
256 }
257 }
258
259 // Compile SQL query
260
261 if ($type == 'posts') {
262 $query = '';
263 $boards = listBoards();
264 if (empty($boards))
265 error(_('There are no boards to search!'));
266
267 foreach ($boards as $board) {
268 openBoard($board['uri']);
269 if (!hasPermission($config['mod']['search_posts'], $board['uri']))
270 continue;
271
272 if (!empty($query))
273 $query .= ' UNION ALL ';
274 $query .= sprintf("SELECT *, '%s' AS `board` FROM ``posts_%s`` WHERE %s", $board['uri'], $board['uri'], $sql_like);
275 }
276
277 // You weren't allowed to search any boards
278 if (empty($query))
279 error($config['error']['noaccess']);
280
281 $query .= ' ORDER BY `sticky` DESC, `id` DESC';
282 }
283
284 if ($type == 'IP_notes') {
285 $query = 'SELECT * FROM ``ip_notes`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE ' . $sql_like . ' ORDER BY `time` DESC';
286 $sql_table = 'ip_notes';
287 if (!hasPermission($config['mod']['view_notes']) || !hasPermission($config['mod']['show_ip']))
288 error($config['error']['noaccess']);
289 }
290
291 if ($type == 'bans') {
292 $query = 'SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `creator` = ``mods``.`id` WHERE ' . $sql_like . ' ORDER BY (`expires` IS NOT NULL AND `expires` < UNIX_TIMESTAMP()), `created` DESC';
293 $sql_table = 'bans';
294 if (!hasPermission($config['mod']['view_banlist']))
295 error($config['error']['noaccess']);
296 }
297
298 if ($type == 'log') {
299 $query = 'SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE ' . $sql_like . ' ORDER BY `time` DESC';
300 $sql_table = 'modlogs';
301 if (!hasPermission($config['mod']['modlog']))
302 error($config['error']['noaccess']);
303 }
304
305 // Execute SQL query (with pages)
306 $q = query($query . ' LIMIT ' . (($page_no - 1) * $config['mod']['search_page']) . ', ' . $config['mod']['search_page']) or error(db_error());
307 $results = $q->fetchAll(PDO::FETCH_ASSOC);
308
309 // Get total result count
310 if ($type == 'posts') {
311 $q = query("SELECT COUNT(*) FROM ($query) AS `tmp_table`") or error(db_error());
312 $result_count = $q->fetchColumn();
313 } else {
314 $q = query('SELECT COUNT(*) FROM `' . $sql_table . '` WHERE ' . $sql_like) or error(db_error());
315 $result_count = $q->fetchColumn();
316 }
317
318 if ($type == 'bans') {
319 foreach ($results as &$ban) {
320 $ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend']));
321 if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false)
322 $ban['single_addr'] = true;
323 }
324 }
325
326 if ($type == 'posts') {
327 foreach ($results as &$post) {
328 $post['snippet'] = pm_snippet($post['body']);
329 }
330 }
331
332 // $results now contains the search results
333
334 mod_page(_('Search results'), 'mod/search_results.html', array(
335 'search_type' => $type,
336 'search_query' => $search_query,
337 'search_query_escaped' => $search_query_escaped,
338 'result_count' => $result_count,
339 'results' => $results
340 ));
341}
342
343function mod_edit_board($boardName) {
344 global $board, $config;
345
346 if (!openBoard($boardName))
347 error($config['error']['noboard']);
348
349 if (!hasPermission($config['mod']['manageboards'], $board['uri']))
350 error($config['error']['noaccess']);
351
352 if (isset($_POST['title'], $_POST['subtitle'])) {
353 if (isset($_POST['delete'])) {
354 if (!hasPermission($config['mod']['manageboards'], $board['uri']))
355 error($config['error']['deleteboard']);
356
357 $query = prepare('DELETE FROM ``boards`` WHERE `uri` = :uri');
358 $query->bindValue(':uri', $board['uri']);
359 $query->execute() or error(db_error($query));
360
361 if ($config['cache']['enabled']) {
362 cache::delete('board_' . $board['uri']);
363 cache::delete('all_boards');
364 }
365
366 modLog('Deleted board: ' . sprintf($config['board_abbreviation'], $board['uri']), false);
367
368 // Delete posting table
369 $query = query(sprintf('DROP TABLE IF EXISTS ``posts_%s``', $board['uri'])) or error(db_error());
370
371 // Clear reports
372 $query = prepare('DELETE FROM ``reports`` WHERE `board` = :id');
373 $query->bindValue(':id', $board['uri'], PDO::PARAM_INT);
374 $query->execute() or error(db_error($query));
375
376 // Delete from table
377 $query = prepare('DELETE FROM ``boards`` WHERE `uri` = :uri');
378 $query->bindValue(':uri', $board['uri'], PDO::PARAM_INT);
379 $query->execute() or error(db_error($query));
380
381 $query = prepare("SELECT `board`, `post` FROM ``cites`` WHERE `target_board` = :board ORDER BY `board`");
382 $query->bindValue(':board', $board['uri']);
383 $query->execute() or error(db_error($query));
384 while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
385 if ($board['uri'] != $cite['board']) {
386 if (!isset($tmp_board))
387 $tmp_board = $board;
388 openBoard($cite['board']);
389 rebuildPost($cite['post']);
390 }
391 }
392
393 if (isset($tmp_board))
394 $board = $tmp_board;
395
396 $query = prepare('DELETE FROM ``cites`` WHERE `board` = :board OR `target_board` = :board');
397 $query->bindValue(':board', $board['uri']);
398 $query->execute() or error(db_error($query));
399
400 $query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board');
401 $query->bindValue(':board', $board['uri']);
402 $query->execute() or error(db_error($query));
403
404 // Remove board from users/permissions table
405 $query = query('SELECT `id`,`boards` FROM ``mods``') or error(db_error());
406 while ($user = $query->fetch(PDO::FETCH_ASSOC)) {
407 $user_boards = explode(',', $user['boards']);
408 if (in_array($board['uri'], $user_boards)) {
409 unset($user_boards[array_search($board['uri'], $user_boards)]);
410 $_query = prepare('UPDATE ``mods`` SET `boards` = :boards WHERE `id` = :id');
411 $_query->bindValue(':boards', implode(',', $user_boards));
412 $_query->bindValue(':id', $user['id']);
413 $_query->execute() or error(db_error($_query));
414 }
415 }
416
417 // Delete entire board directory
418 rrmdir($board['uri'] . '/');
419 } else {
420 $query = prepare('UPDATE ``boards`` SET `title` = :title, `subtitle` = :subtitle WHERE `uri` = :uri');
421 $query->bindValue(':uri', $board['uri']);
422 $query->bindValue(':title', $_POST['title']);
423 $query->bindValue(':subtitle', $_POST['subtitle']);
424 $query->execute() or error(db_error($query));
425
426 modLog('Edited board information for ' . sprintf($config['board_abbreviation'], $board['uri']), false);
427 }
428
429 if ($config['cache']['enabled']) {
430 cache::delete('board_' . $board['uri']);
431 cache::delete('all_boards');
432 }
433
434 rebuildThemes('boards');
435
436 header('Location: ?/', true, $config['redirect_http']);
437 } else {
438 mod_page(sprintf('%s: ' . $config['board_abbreviation'], _('Edit board'), $board['uri']), 'mod/board.html', array(
439 'board' => $board,
440 'token' => make_secure_link_token('edit/' . $board['uri'])
441 ));
442 }
443}
444
445function mod_new_board() {
446 global $config, $board;
447
448 if (!hasPermission($config['mod']['newboard']))
449 error($config['error']['noaccess']);
450
451 if (isset($_POST['uri'], $_POST['title'], $_POST['subtitle'])) {
452 if ($_POST['uri'] == '')
453 error(sprintf($config['error']['required'], 'URI'));
454
455 if ($_POST['title'] == '')
456 error(sprintf($config['error']['required'], 'title'));
457
458 if (!preg_match('/^' . $config['board_regex'] . '$/u', $_POST['uri']))
459 error(sprintf($config['error']['invalidfield'], 'URI'));
460
461 $bytes = 0;
462 $chars = preg_split('//u', $_POST['uri'], -1, PREG_SPLIT_NO_EMPTY);
463 foreach ($chars as $char) {
464 $o = 0;
465 $ord = ordutf8($char, $o);
466 if ($ord > 0x0080)
467 $bytes += 5; // @01ff
468 else
469 $bytes ++;
470 }
471 $bytes + strlen('posts_.frm');
472
473 if ($bytes > 255) {
474 error('Your filesystem cannot handle a board URI of that length (' . $bytes . '/255 bytes)');
475 exit;
476 }
477
478 if (openBoard($_POST['uri'])) {
479 error(sprintf($config['error']['boardexists'], $board['url']));
480 }
481
482 $query = prepare('INSERT INTO ``boards`` VALUES (:uri, :title, :subtitle)');
483 $query->bindValue(':uri', $_POST['uri']);
484 $query->bindValue(':title', $_POST['title']);
485 $query->bindValue(':subtitle', $_POST['subtitle']);
486 $query->execute() or error(db_error($query));
487
488 modLog('Created a new board: ' . sprintf($config['board_abbreviation'], $_POST['uri']));
489
490 if (!openBoard($_POST['uri']))
491 error(_("Couldn't open board after creation."));
492
493 $query = Element('posts.sql', array('board' => $board['uri']));
494
495 if (mysql_version() < 50503)
496 $query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
497
498 query($query) or error(db_error());
499
500 if ($config['cache']['enabled'])
501 cache::delete('all_boards');
502
503 // Build the board
504 buildIndex();
505
506 rebuildThemes('boards');
507
508 header('Location: ?/' . $board['uri'] . '/' . $config['file_index'], true, $config['redirect_http']);
509 }
510
511 mod_page(_('New board'), 'mod/board.html', array('new' => true, 'token' => make_secure_link_token('new-board')));
512}
513
514function mod_noticeboard($page_no = 1) {
515 global $config, $pdo, $mod;
516
517 if ($page_no < 1)
518 error($config['error']['404']);
519
520 if (!hasPermission($config['mod']['noticeboard']))
521 error($config['error']['noaccess']);
522
523 if (isset($_POST['subject'], $_POST['body'])) {
524 if (!hasPermission($config['mod']['noticeboard_post']))
525 error($config['error']['noaccess']);
526
527 $_POST['body'] = escape_markup_modifiers($_POST['body']);
528 markup($_POST['body']);
529
530 $query = prepare('INSERT INTO ``noticeboard`` VALUES (NULL, :mod, :time, :subject, :body)');
531 $query->bindValue(':mod', $mod['id']);
532 $query->bindvalue(':time', time());
533 $query->bindValue(':subject', $_POST['subject']);
534 $query->bindValue(':body', $_POST['body']);
535 $query->execute() or error(db_error($query));
536
537 if ($config['cache']['enabled'])
538 cache::delete('noticeboard_preview');
539
540 modLog('Posted a noticeboard entry');
541
542 header('Location: ?/noticeboard#' . $pdo->lastInsertId(), true, $config['redirect_http']);
543 }
544
545 $query = prepare("SELECT ``noticeboard``.*, `username` FROM ``noticeboard`` LEFT JOIN ``mods`` ON ``mods``.`id` = `mod` ORDER BY `id` DESC LIMIT :offset, :limit");
546 $query->bindValue(':limit', $config['mod']['noticeboard_page'], PDO::PARAM_INT);
547 $query->bindValue(':offset', ($page_no - 1) * $config['mod']['noticeboard_page'], PDO::PARAM_INT);
548 $query->execute() or error(db_error($query));
549 $noticeboard = $query->fetchAll(PDO::FETCH_ASSOC);
550
551 if (empty($noticeboard) && $page_no > 1)
552 error($config['error']['404']);
553
554 foreach ($noticeboard as &$entry) {
555 $entry['delete_token'] = make_secure_link_token('noticeboard/delete/' . $entry['id']);
556 }
557
558 $query = prepare("SELECT COUNT(*) FROM ``noticeboard``");
559 $query->execute() or error(db_error($query));
560 $count = $query->fetchColumn();
561
562 mod_page(_('Noticeboard'), 'mod/noticeboard.html', array(
563 'noticeboard' => $noticeboard,
564 'count' => $count,
565 'token' => make_secure_link_token('noticeboard')
566 ));
567}
568
569function mod_noticeboard_delete($id) {
570 global $config;
571
572 if (!hasPermission($config['mod']['noticeboard_delete']))
573 error($config['error']['noaccess']);
574
575 $query = prepare('DELETE FROM ``noticeboard`` WHERE `id` = :id');
576 $query->bindValue(':id', $id);
577 $query->execute() or error(db_error($query));
578
579 modLog('Deleted a noticeboard entry');
580
581 if ($config['cache']['enabled'])
582 cache::delete('noticeboard_preview');
583
584 header('Location: ?/noticeboard', true, $config['redirect_http']);
585}
586
587function mod_news($page_no = 1) {
588 global $config, $pdo, $mod;
589
590 if ($page_no < 1)
591 error($config['error']['404']);
592
593 if (isset($_POST['subject'], $_POST['body'])) {
594 if (!hasPermission($config['mod']['news']))
595 error($config['error']['noaccess']);
596
597 $_POST['body'] = escape_markup_modifiers($_POST['body']);
598 markup($_POST['body']);
599
600 $query = prepare('INSERT INTO ``news`` VALUES (NULL, :name, :time, :subject, :body)');
601 $query->bindValue(':name', isset($_POST['name']) && hasPermission($config['mod']['news_custom']) ? $_POST['name'] : $mod['username']);
602 $query->bindvalue(':time', time());
603 $query->bindValue(':subject', $_POST['subject']);
604 $query->bindValue(':body', $_POST['body']);
605 $query->execute() or error(db_error($query));
606
607 modLog('Posted a news entry');
608
609 rebuildThemes('news');
610
611 header('Location: ?/news#' . $pdo->lastInsertId(), true, $config['redirect_http']);
612 }
613
614 $query = prepare("SELECT * FROM ``news`` ORDER BY `id` DESC LIMIT :offset, :limit");
615 $query->bindValue(':limit', $config['mod']['news_page'], PDO::PARAM_INT);
616 $query->bindValue(':offset', ($page_no - 1) * $config['mod']['news_page'], PDO::PARAM_INT);
617 $query->execute() or error(db_error($query));
618 $news = $query->fetchAll(PDO::FETCH_ASSOC);
619
620 if (empty($news) && $page_no > 1)
621 error($config['error']['404']);
622
623 foreach ($news as &$entry) {
624 $entry['delete_token'] = make_secure_link_token('news/delete/' . $entry['id']);
625 }
626
627 $query = prepare("SELECT COUNT(*) FROM ``news``");
628 $query->execute() or error(db_error($query));
629 $count = $query->fetchColumn();
630
631 mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('news')));
632}
633
634function mod_news_delete($id) {
635 global $config;
636
637 if (!hasPermission($config['mod']['news_delete']))
638 error($config['error']['noaccess']);
639
640 $query = prepare('DELETE FROM ``news`` WHERE `id` = :id');
641 $query->bindValue(':id', $id);
642 $query->execute() or error(db_error($query));
643
644 modLog('Deleted a news entry');
645
646 header('Location: ?/news', true, $config['redirect_http']);
647}
648
649function mod_log($page_no = 1) {
650 global $config;
651
652 if ($page_no < 1)
653 error($config['error']['404']);
654
655 if (!hasPermission($config['mod']['modlog']))
656 error($config['error']['noaccess']);
657
658 $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` ORDER BY `time` DESC LIMIT :offset, :limit");
659 $query->bindValue(':limit', $config['mod']['modlog_page'], PDO::PARAM_INT);
660 $query->bindValue(':offset', ($page_no - 1) * $config['mod']['modlog_page'], PDO::PARAM_INT);
661 $query->execute() or error(db_error($query));
662 $logs = $query->fetchAll(PDO::FETCH_ASSOC);
663
664 if (empty($logs) && $page_no > 1)
665 error($config['error']['404']);
666
667 $query = prepare("SELECT COUNT(*) FROM ``modlogs``");
668 $query->execute() or error(db_error($query));
669 $count = $query->fetchColumn();
670
671 mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count));
672}
673
674function mod_user_log($username, $page_no = 1) {
675 global $config;
676
677 if ($page_no < 1)
678 error($config['error']['404']);
679
680 if (!hasPermission($config['mod']['modlog']))
681 error($config['error']['noaccess']);
682
683 $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `username` = :username ORDER BY `time` DESC LIMIT :offset, :limit");
684 $query->bindValue(':username', $username);
685 $query->bindValue(':limit', $config['mod']['modlog_page'], PDO::PARAM_INT);
686 $query->bindValue(':offset', ($page_no - 1) * $config['mod']['modlog_page'], PDO::PARAM_INT);
687 $query->execute() or error(db_error($query));
688 $logs = $query->fetchAll(PDO::FETCH_ASSOC);
689
690 if (empty($logs) && $page_no > 1)
691 error($config['error']['404']);
692
693 $query = prepare("SELECT COUNT(*) FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `username` = :username");
694 $query->bindValue(':username', $username);
695 $query->execute() or error(db_error($query));
696 $count = $query->fetchColumn();
697
698 mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'username' => $username));
699}
700
701function mod_view_board($boardName, $page_no = 1) {
702 global $config, $mod;
703
704 if (!openBoard($boardName))
705 error($config['error']['noboard']);
706
707 if (!$page = index($page_no, $mod)) {
708 error($config['error']['404']);
709 }
710
711 $page['pages'] = getPages(true);
712 $page['pages'][$page_no-1]['selected'] = true;
713 $page['btn'] = getPageButtons($page['pages'], true);
714 $page['mod'] = true;
715 $page['config'] = $config;
716
717 echo Element('index.html', $page);
718}
719
720function mod_view_thread($boardName, $thread) {
721 global $config, $mod;
722
723 if (!openBoard($boardName))
724 error($config['error']['noboard']);
725
726 $page = buildThread($thread, true, $mod);
727 echo $page;
728}
729
730function mod_view_thread50($boardName, $thread) {
731 global $config, $mod;
732
733 if (!openBoard($boardName))
734 error($config['error']['noboard']);
735
736 $page = buildThread50($thread, true, $mod);
737 echo $page;
738}
739
740function mod_ip_remove_note($ip, $id) {
741 global $config, $mod;
742
743 if (!hasPermission($config['mod']['remove_notes']))
744 error($config['error']['noaccess']);
745
746 if (filter_var($ip, FILTER_VALIDATE_IP) === false)
747 error("Invalid IP address.");
748
749 $query = prepare('DELETE FROM ``ip_notes`` WHERE `ip` = :ip AND `id` = :id');
750 $query->bindValue(':ip', $ip);
751 $query->bindValue(':id', $id);
752 $query->execute() or error(db_error($query));
753
754 modLog("Removed a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
755
756 header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
757}
758
759function mod_page_ip($ip) {
760 global $config, $mod;
761
762 if (filter_var($ip, FILTER_VALIDATE_IP) === false)
763 error("Invalid IP address.");
764
765 if (isset($_POST['ban_id'], $_POST['unban'])) {
766 if (!hasPermission($config['mod']['unban']))
767 error($config['error']['noaccess']);
768
769 Bans::delete($_POST['ban_id'], true);
770
771 header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']);
772 return;
773 }
774
775 if (isset($_POST['note'])) {
776 if (!hasPermission($config['mod']['create_notes']))
777 error($config['error']['noaccess']);
778
779 $_POST['note'] = escape_markup_modifiers($_POST['note']);
780 markup($_POST['note']);
781 $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
782 $query->bindValue(':ip', $ip);
783 $query->bindValue(':mod', $mod['id']);
784 $query->bindValue(':time', time());
785 $query->bindValue(':body', $_POST['note']);
786 $query->execute() or error(db_error($query));
787
788 modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
789
790 header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
791 return;
792 }
793
794 $args = array();
795 $args['ip'] = $ip;
796 $args['posts'] = array();
797
798 if ($config['mod']['dns_lookup'])
799 $args['hostname'] = rDNS($ip);
800
801 $boards = listBoards();
802 foreach ($boards as $board) {
803 openBoard($board['uri']);
804 if (!hasPermission($config['mod']['show_ip'], $board['uri']))
805 continue;
806 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $board['uri']));
807 $query->bindValue(':ip', $ip);
808 $query->bindValue(':limit', $config['mod']['ip_recentposts'], PDO::PARAM_INT);
809 $query->execute() or error(db_error($query));
810
811 while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
812 if (!$post['thread']) {
813 $po = new Thread($post, '?/', $mod, false);
814 } else {
815 $po = new Post($post, '?/', $mod);
816 }
817
818 if (!isset($args['posts'][$board['uri']]))
819 $args['posts'][$board['uri']] = array('board' => $board, 'posts' => array());
820 $args['posts'][$board['uri']]['posts'][] = $po->build(true);
821 }
822 }
823
824 $args['boards'] = $boards;
825 $args['token'] = make_secure_link_token('ban');
826
827 if (hasPermission($config['mod']['view_ban'])) {
828 $args['bans'] = Bans::find($ip, false, true);
829 }
830
831 if (hasPermission($config['mod']['view_notes'])) {
832 $query = prepare("SELECT ``ip_notes``.*, `username` FROM ``ip_notes`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `ip` = :ip ORDER BY `time` DESC");
833 $query->bindValue(':ip', $ip);
834 $query->execute() or error(db_error($query));
835 $args['notes'] = $query->fetchAll(PDO::FETCH_ASSOC);
836 }
837
838 if (hasPermission($config['mod']['modlog_ip'])) {
839 $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50");
840 $query->bindValue(':search', '%' . $ip . '%');
841 $query->execute() or error(db_error($query));
842 $args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC);
843 } else {
844 $args['logs'] = array();
845 }
846
847 $args['security_token'] = make_secure_link_token('IP/' . $ip);
848
849 mod_page(sprintf('%s: %s', _('IP'), $ip), 'mod/view_ip.html', $args, $args['hostname']);
850}
851
852function mod_ban() {
853 global $config;
854
855 if (!hasPermission($config['mod']['ban']))
856 error($config['error']['noaccess']);
857
858 if (!isset($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'])) {
859 mod_page(_('New ban'), 'mod/ban_form.html', array('token' => make_secure_link_token('ban')));
860 return;
861 }
862
863 require_once 'inc/mod/ban.php';
864
865 Bans::new_ban($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'] == '*' ? false : $_POST['board']);
866
867 if (isset($_POST['redirect']))
868 header('Location: ' . $_POST['redirect'], true, $config['redirect_http']);
869 else
870 header('Location: ?/', true, $config['redirect_http']);
871}
872
873function mod_bans($page_no = 1) {
874 global $config;
875
876 if ($page_no < 1)
877 error($config['error']['404']);
878
879 if (!hasPermission($config['mod']['view_banlist']))
880 error($config['error']['noaccess']);
881
882 if (isset($_POST['unban'])) {
883 if (!hasPermission($config['mod']['unban']))
884 error($config['error']['noaccess']);
885
886 $unban = array();
887 foreach ($_POST as $name => $unused) {
888 if (preg_match('/^ban_(\d+)$/', $name, $match))
889 $unban[] = $match[1];
890 }
891 if (isset($config['mod']['unban_limit']) && $config['mod']['unban_limit'] && count($unban) > $config['mod']['unban_limit'])
892 error(sprintf($config['error']['toomanyunban'], $config['mod']['unban_limit'], count($unban)));
893
894 foreach ($unban as $id) {
895 Bans::delete($id, true);
896 }
897 header('Location: ?/bans', true, $config['redirect_http']);
898 return;
899 }
900
901 $bans = Bans::list_all(($page_no - 1) * $config['mod']['banlist_page'], $config['mod']['banlist_page']);
902
903 if (empty($bans) && $page_no > 1)
904 error($config['error']['404']);
905
906 foreach ($bans as &$ban) {
907 if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false)
908 $ban['single_addr'] = true;
909 }
910
911 mod_page(_('Ban list'), 'mod/ban_list.html', array(
912 'bans' => $bans,
913 'count' => Bans::count(),
914 'token' => make_secure_link_token('bans')
915 ));
916}
917
918function mod_ban_appeals() {
919 global $config, $board;
920
921 if (!hasPermission($config['mod']['view_ban_appeals']))
922 error($config['error']['noaccess']);
923
924 // Remove stale ban appeals
925 query("DELETE FROM ``ban_appeals`` WHERE NOT EXISTS (SELECT 1 FROM ``bans`` WHERE `ban_id` = ``bans``.`id`)")
926 or error(db_error());
927
928 if (isset($_POST['appeal_id']) && (isset($_POST['unban']) || isset($_POST['deny']))) {
929 if (!hasPermission($config['mod']['ban_appeals']))
930 error($config['error']['noaccess']);
931
932 $query = query("SELECT *, ``ban_appeals``.`id` AS `id` FROM ``ban_appeals``
933 LEFT JOIN ``bans`` ON `ban_id` = ``bans``.`id`
934 WHERE ``ban_appeals``.`id` = " . (int)$_POST['appeal_id']) or error(db_error());
935 if (!$ban = $query->fetch(PDO::FETCH_ASSOC)) {
936 error(_('Ban appeal not found!'));
937 }
938
939 $ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend']));
940
941 if (isset($_POST['unban'])) {
942 modLog('Accepted ban appeal #' . $ban['id'] . ' for ' . $ban['mask']);
943 Bans::delete($ban['ban_id'], true);
944 query("DELETE FROM ``ban_appeals`` WHERE `id` = " . $ban['id']) or error(db_error());
945 } else {
946 modLog('Denied ban appeal #' . $ban['id'] . ' for ' . $ban['mask']);
947 query("UPDATE ``ban_appeals`` SET `denied` = 1 WHERE `id` = " . $ban['id']) or error(db_error());
948 }
949
950 header('Location: ?/ban-appeals', true, $config['redirect_http']);
951 return;
952 }
953
954 $query = query("SELECT *, ``ban_appeals``.`id` AS `id` FROM ``ban_appeals``
955 LEFT JOIN ``bans`` ON `ban_id` = ``bans``.`id`
956 WHERE `denied` != 1 ORDER BY `time`") or error(db_error());
957 $ban_appeals = $query->fetchAll(PDO::FETCH_ASSOC);
958 foreach ($ban_appeals as &$ban) {
959 if ($ban['post'])
960 $ban['post'] = json_decode($ban['post'], true);
961 $ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend']));
962
963 if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) {
964 if (openBoard($ban['post']['board'])) {
965 $query = query(sprintf("SELECT `num_files`, `files` FROM ``posts_%s`` WHERE `id` = " .
966 (int)$ban['post']['id'], $board['uri']));
967 if ($_post = $query->fetch(PDO::FETCH_ASSOC)) {
968 $_post['files'] = $_post['files'] ? json_decode($_post['files']) : array();
969 $ban['post'] = array_merge($ban['post'], $_post);
970 } else {
971 $ban['post']['files'] = array(array());
972 $ban['post']['files'][0]['file'] = 'deleted';
973 $ban['post']['files'][0]['thumb'] = false;
974 $ban['post']['num_files'] = 1;
975 }
976 } else {
977 $ban['post']['files'] = array(array());
978 $ban['post']['files'][0]['file'] = 'deleted';
979 $ban['post']['files'][0]['thumb'] = false;
980 $ban['post']['num_files'] = 1;
981 }
982
983 if ($ban['post']['thread']) {
984 $ban['post'] = new Post($ban['post']);
985 } else {
986 $ban['post'] = new Thread($ban['post'], null, false, false);
987 }
988 }
989 }
990
991 mod_page(_('Ban appeals'), 'mod/ban_appeals.html', array(
992 'ban_appeals' => $ban_appeals,
993 'token' => make_secure_link_token('ban-appeals')
994 ));
995}
996
997function mod_lock($board, $unlock, $post) {
998 global $config;
999
1000 if (!openBoard($board))
1001 error($config['error']['noboard']);
1002
1003 if (!hasPermission($config['mod']['lock'], $board))
1004 error($config['error']['noaccess']);
1005
1006 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `locked` = :locked WHERE `id` = :id AND `thread` IS NULL', $board));
1007 $query->bindValue(':id', $post);
1008 $query->bindValue(':locked', $unlock ? 0 : 1);
1009 $query->execute() or error(db_error($query));
1010 if ($query->rowCount()) {
1011 modLog(($unlock ? 'Unlocked' : 'Locked') . " thread #{$post}");
1012 buildThread($post);
1013 buildIndex();
1014 }
1015
1016 if ($config['mod']['dismiss_reports_on_lock']) {
1017 $query = prepare('DELETE FROM ``reports`` WHERE `board` = :board AND `post` = :id');
1018 $query->bindValue(':board', $board);
1019 $query->bindValue(':id', $post);
1020 $query->execute() or error(db_error($query));
1021 }
1022
1023 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1024
1025 if ($unlock)
1026 event('unlock', $post);
1027 else
1028 event('lock', $post);
1029}
1030
1031function mod_sticky($board, $unsticky, $post) {
1032 global $config;
1033
1034 if (!openBoard($board))
1035 error($config['error']['noboard']);
1036
1037 if (!hasPermission($config['mod']['sticky'], $board))
1038 error($config['error']['noaccess']);
1039
1040 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `sticky` = :sticky WHERE `id` = :id AND `thread` IS NULL', $board));
1041 $query->bindValue(':id', $post);
1042 $query->bindValue(':sticky', $unsticky ? 0 : 1);
1043 $query->execute() or error(db_error($query));
1044 if ($query->rowCount()) {
1045 modLog(($unsticky ? 'Unstickied' : 'Stickied') . " thread #{$post}");
1046 buildThread($post);
1047 buildIndex();
1048 }
1049
1050 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1051}
1052
1053function mod_bumplock($board, $unbumplock, $post) {
1054 global $config;
1055
1056 if (!openBoard($board))
1057 error($config['error']['noboard']);
1058
1059 if (!hasPermission($config['mod']['bumplock'], $board))
1060 error($config['error']['noaccess']);
1061
1062 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `sage` = :bumplock WHERE `id` = :id AND `thread` IS NULL', $board));
1063 $query->bindValue(':id', $post);
1064 $query->bindValue(':bumplock', $unbumplock ? 0 : 1);
1065 $query->execute() or error(db_error($query));
1066 if ($query->rowCount()) {
1067 modLog(($unbumplock ? 'Unbumplocked' : 'Bumplocked') . " thread #{$post}");
1068 buildThread($post);
1069 buildIndex();
1070 }
1071
1072 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1073}
1074
1075function mod_move_reply($originBoard, $postID) {
1076 global $board, $config, $mod;
1077
1078 if (!openBoard($originBoard))
1079 error($config['error']['noboard']);
1080
1081 if (!hasPermission($config['mod']['move'], $originBoard))
1082 error($config['error']['noaccess']);
1083
1084 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id', $originBoard));
1085 $query->bindValue(':id', $postID);
1086 $query->execute() or error(db_error($query));
1087 if (!$post = $query->fetch(PDO::FETCH_ASSOC))
1088 error($config['error']['404']);
1089
1090 if (isset($_POST['board'])) {
1091 $targetBoard = $_POST['board'];
1092
1093 if ($_POST['target_thread']) {
1094 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id', $targetBoard));
1095 $query->bindValue(':id', $_POST['target_thread']);
1096 $query->execute() or error(db_error($query)); // If it fails, thread probably does not exist
1097 $post['op'] = false;
1098 $post['thread'] = $_POST['target_thread'];
1099 }
1100 else {
1101 $post['op'] = true;
1102 }
1103
1104 if ($post['files']) {
1105 $post['files'] = json_decode($post['files'], TRUE);
1106 $post['has_file'] = true;
1107 foreach ($post['files'] as $i => &$file) {
1108 $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file'];
1109 $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb'];
1110 }
1111 } else {
1112 $post['has_file'] = false;
1113 }
1114
1115 // allow thread to keep its same traits (stickied, locked, etc.)
1116 $post['mod'] = true;
1117
1118 if (!openBoard($targetBoard))
1119 error($config['error']['noboard']);
1120
1121 // create the new post
1122 $newID = post($post);
1123
1124 if ($post['has_file']) {
1125 foreach ($post['files'] as $i => &$file) {
1126 // move the image
1127 rename($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']);
1128 if ($file['thumb'] != 'spoiler') { //trying to move/copy the spoiler thumb raises an error
1129 rename($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']);
1130 }
1131 }
1132 }
1133
1134 // build index
1135 buildIndex();
1136 // build new thread
1137 buildThread($newID);
1138
1139 // trigger themes
1140 rebuildThemes('post', $targetBoard);
1141 // mod log
1142 modLog("Moved post #${postID} to " . sprintf($config['board_abbreviation'], $targetBoard) . " (#${newID})", $originBoard);
1143
1144 // return to original board
1145 openBoard($originBoard);
1146
1147 // delete original post
1148 deletePost($postID);
1149 buildIndex();
1150
1151 // open target board for redirect
1152 openBoard($targetBoard);
1153
1154 // Find new thread on our target board
1155 $query = prepare(sprintf('SELECT thread FROM ``posts_%s`` WHERE `id` = :id', $targetBoard));
1156 $query->bindValue(':id', $newID);
1157 $query->execute() or error(db_error($query));
1158 $post = $query->fetch(PDO::FETCH_ASSOC);
1159
1160 // redirect
1161 header('Location: ?/' . sprintf($config['board_path'], $board['uri']) . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $newID) . '#' . $newID, true, $config['redirect_http']);
1162 }
1163
1164 else {
1165 $boards = listBoards();
1166
1167 $security_token = make_secure_link_token($originBoard . '/move_reply/' . $postID);
1168
1169 mod_page(_('Move reply'), 'mod/move_reply.html', array('post' => $postID, 'board' => $originBoard, 'boards' => $boards, 'token' => $security_token));
1170
1171 }
1172
1173}
1174
1175function mod_move($originBoard, $postID) {
1176 global $board, $config, $mod, $pdo;
1177
1178 if (!openBoard($originBoard))
1179 error($config['error']['noboard']);
1180
1181 if (!hasPermission($config['mod']['move'], $originBoard))
1182 error($config['error']['noaccess']);
1183
1184 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL', $originBoard));
1185 $query->bindValue(':id', $postID);
1186 $query->execute() or error(db_error($query));
1187 if (!$post = $query->fetch(PDO::FETCH_ASSOC))
1188 error($config['error']['404']);
1189
1190 if (isset($_POST['board'])) {
1191 $targetBoard = $_POST['board'];
1192 $shadow = isset($_POST['shadow']);
1193
1194 if ($targetBoard === $originBoard)
1195 error(_('Target and source board are the same.'));
1196
1197 // copy() if leaving a shadow thread behind; else, rename().
1198 $clone = $shadow ? 'copy' : 'rename';
1199
1200 // indicate that the post is a thread
1201 $post['op'] = true;
1202
1203 if ($post['files']) {
1204 $post['files'] = json_decode($post['files'], TRUE);
1205 $post['has_file'] = true;
1206 foreach ($post['files'] as $i => &$file) {
1207 if ($file['file'] === 'deleted')
1208 continue;
1209 $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file'];
1210 $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb'];
1211 }
1212 } else {
1213 $post['has_file'] = false;
1214 }
1215
1216 // allow thread to keep its same traits (stickied, locked, etc.)
1217 $post['mod'] = true;
1218
1219 if (!openBoard($targetBoard))
1220 error($config['error']['noboard']);
1221
1222 // create the new thread
1223 $newID = post($post);
1224
1225 if ($post['has_file']) {
1226 // copy image
1227 foreach ($post['files'] as $i => &$file) {
1228 if ($file['file'] !== 'deleted')
1229 $clone($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']);
1230 if (isset($file['thumb']) && !in_array($file['thumb'], array('spoiler', 'deleted', 'file')))
1231 $clone($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']);
1232 }
1233 }
1234
1235 // go back to the original board to fetch replies
1236 openBoard($originBoard);
1237
1238 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id`', $originBoard));
1239 $query->bindValue(':id', $postID, PDO::PARAM_INT);
1240 $query->execute() or error(db_error($query));
1241
1242 $replies = array();
1243
1244 while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
1245 $post['mod'] = true;
1246 $post['thread'] = $newID;
1247
1248 if ($post['files']) {
1249 $post['files'] = json_decode($post['files'], TRUE);
1250 $post['has_file'] = true;
1251 foreach ($post['files'] as $i => &$file) {
1252 $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file'];
1253 $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb'];
1254 }
1255 } else {
1256 $post['has_file'] = false;
1257 }
1258
1259 $replies[] = $post;
1260 }
1261
1262 $newIDs = array($postID => $newID);
1263
1264 openBoard($targetBoard);
1265
1266 foreach ($replies as &$post) {
1267 $query = prepare('SELECT `target` FROM ``cites`` WHERE `target_board` = :board AND `board` = :board AND `post` = :post');
1268 $query->bindValue(':board', $originBoard);
1269 $query->bindValue(':post', $post['id'], PDO::PARAM_INT);
1270 $query->execute() or error(db_error($qurey));
1271
1272 // correct >>X links
1273 while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
1274 if (isset($newIDs[$cite['target']])) {
1275 $post['body_nomarkup'] = preg_replace(
1276 '/(>>(>\/' . preg_quote($originBoard, '/') . '\/)?)' . preg_quote($cite['target'], '/') . '/',
1277 '>>' . $newIDs[$cite['target']],
1278 $post['body_nomarkup']);
1279
1280 $post['body'] = $post['body_nomarkup'];
1281 }
1282 }
1283
1284 $post['body'] = $post['body_nomarkup'];
1285
1286 $post['op'] = false;
1287 $post['tracked_cites'] = markup($post['body'], true);
1288
1289 if ($post['has_file']) {
1290 // copy image
1291 foreach ($post['files'] as $i => &$file) {
1292 $clone($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']);
1293 $clone($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']);
1294 }
1295 }
1296 // insert reply
1297 $newIDs[$post['id']] = $newPostID = post($post);
1298
1299
1300 if (!empty($post['tracked_cites'])) {
1301 $insert_rows = array();
1302 foreach ($post['tracked_cites'] as $cite) {
1303 $insert_rows[] = '(' .
1304 $pdo->quote($board['uri']) . ', ' . $newPostID . ', ' .
1305 $pdo->quote($cite[0]) . ', ' . (int)$cite[1] . ')';
1306 }
1307 query('INSERT INTO ``cites`` VALUES ' . implode(', ', $insert_rows)) or error(db_error());
1308 }
1309 }
1310
1311 modLog("Moved thread #${postID} to " . sprintf($config['board_abbreviation'], $targetBoard) . " (#${newID})", $originBoard);
1312
1313 // build new thread
1314 buildThread($newID);
1315
1316 clean();
1317 buildIndex();
1318
1319 // trigger themes
1320 rebuildThemes('post', $targetBoard);
1321
1322 // return to original board
1323 openBoard($originBoard);
1324
1325 if ($shadow) {
1326 // lock old thread
1327 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `locked` = 1 WHERE `id` = :id', $originBoard));
1328 $query->bindValue(':id', $postID, PDO::PARAM_INT);
1329 $query->execute() or error(db_error($query));
1330
1331 // leave a reply, linking to the new thread
1332 $post = array(
1333 'mod' => true,
1334 'subject' => '',
1335 'email' => '',
1336 'name' => (!$config['mod']['shadow_name'] ? $config['anonymous'] : $config['mod']['shadow_name']),
1337 'capcode' => $config['mod']['shadow_capcode'],
1338 'trip' => '',
1339 'password' => '',
1340 'has_file' => false,
1341 // attach to original thread
1342 'thread' => $postID,
1343 'op' => false
1344 );
1345
1346 $post['body'] = $post['body_nomarkup'] = sprintf($config['mod']['shadow_mesage'], '>>>/' . $targetBoard . '/' . $newID);
1347
1348 markup($post['body']);
1349
1350 $botID = post($post);
1351 buildThread($postID);
1352
1353 buildIndex();
1354
1355 header('Location: ?/' . sprintf($config['board_path'], $originBoard) . $config['dir']['res'] .sprintf($config['file_page'], $postID) .
1356 '#' . $botID, true, $config['redirect_http']);
1357 } else {
1358 deletePost($postID);
1359 buildIndex();
1360
1361 openBoard($targetBoard);
1362 header('Location: ?/' . sprintf($config['board_path'], $board['uri']) . $config['dir']['res'] . sprintf($config['file_page'], $newID), true, $config['redirect_http']);
1363 }
1364 }
1365
1366 $boards = listBoards();
1367 if (count($boards) <= 1)
1368 error(_('Impossible to move thread; there is only one board.'));
1369
1370 $security_token = make_secure_link_token($originBoard . '/move/' . $postID);
1371
1372 mod_page(_('Move thread'), 'mod/move.html', array('post' => $postID, 'board' => $originBoard, 'boards' => $boards, 'token' => $security_token));
1373}
1374
1375function mod_ban_post($board, $delete, $post, $token = false) {
1376 global $config, $mod;
1377
1378 if (!openBoard($board))
1379 error($config['error']['noboard']);
1380
1381 if (!hasPermission($config['mod']['delete'], $board))
1382 error($config['error']['noaccess']);
1383
1384 $security_token = make_secure_link_token($board . '/ban/' . $post);
1385
1386 $query = prepare(sprintf('SELECT ' . ($config['ban_show_post'] ? '*' : '`ip`, `thread`') .
1387 ' FROM ``posts_%s`` WHERE `id` = :id', $board));
1388 $query->bindValue(':id', $post);
1389 $query->execute() or error(db_error($query));
1390 if (!$_post = $query->fetch(PDO::FETCH_ASSOC))
1391 error($config['error']['404']);
1392
1393 $thread = $_post['thread'];
1394 $ip = $_post['ip'];
1395
1396 if (isset($_POST['new_ban'], $_POST['reason'], $_POST['length'], $_POST['board'])) {
1397 require_once 'inc/mod/ban.php';
1398
1399 if (isset($_POST['ip']))
1400 $ip = $_POST['ip'];
1401
1402 Bans::new_ban($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'] == '*' ? false : $_POST['board'],
1403 false, $config['ban_show_post'] ? $_post : false);
1404
1405 if (isset($_POST['public_message'], $_POST['message'])) {
1406 // public ban message
1407 $length_english = Bans::parse_time($_POST['length']) ? 'for ' . until(Bans::parse_time($_POST['length'])) : 'permanently';
1408 $_POST['message'] = preg_replace('/[\r\n]/', '', $_POST['message']);
1409 $_POST['message'] = str_replace('%length%', $length_english, $_POST['message']);
1410 $_POST['message'] = str_replace('%LENGTH%', strtoupper($length_english), $_POST['message']);
1411 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `body_nomarkup` = CONCAT(`body_nomarkup`, :body_nomarkup) WHERE `id` = :id', $board));
1412 $query->bindValue(':id', $post);
1413 $query->bindValue(':body_nomarkup', sprintf("\n<tinyboard ban message>%s</tinyboard>", utf8tohtml($_POST['message'])));
1414 $query->execute() or error(db_error($query));
1415 rebuildPost($post);
1416
1417 modLog("Attached a public ban message to post #{$post}: " . utf8tohtml($_POST['message']));
1418 buildThread($thread ? $thread : $post);
1419 buildIndex();
1420 } elseif (isset($_POST['delete']) && (int) $_POST['delete']) {
1421 // Delete post
1422 deletePost($post);
1423 modLog("Deleted post #{$post}");
1424 // Rebuild board
1425 buildIndex();
1426 // Rebuild themes
1427 rebuildThemes('post-delete', $board);
1428 }
1429
1430 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1431 }
1432
1433 $args = array(
1434 'ip' => $ip,
1435 'hide_ip' => !hasPermission($config['mod']['show_ip'], $board),
1436 'post' => $post,
1437 'board' => $board,
1438 'delete' => (bool)$delete,
1439 'boards' => listBoards(),
1440 'token' => $security_token
1441 );
1442
1443 mod_page(_('New ban'), 'mod/ban_form.html', $args);
1444}
1445
1446function mod_edit_post($board, $edit_raw_html, $postID) {
1447 global $config, $mod;
1448
1449 if (!openBoard($board))
1450 error($config['error']['noboard']);
1451
1452 if (!hasPermission($config['mod']['editpost'], $board))
1453 error($config['error']['noaccess']);
1454
1455 if ($edit_raw_html && !hasPermission($config['mod']['rawhtml'], $board))
1456 error($config['error']['noaccess']);
1457
1458 $security_token = make_secure_link_token($board . '/edit' . ($edit_raw_html ? '_raw' : '') . '/' . $postID);
1459
1460 $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id', $board));
1461 $query->bindValue(':id', $postID);
1462 $query->execute() or error(db_error($query));
1463
1464 if (!$post = $query->fetch(PDO::FETCH_ASSOC))
1465 error($config['error']['404']);
1466
1467 if (isset($_POST['name'], $_POST['email'], $_POST['subject'], $_POST['body'])) {
1468 if ($edit_raw_html)
1469 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `name` = :name, `email` = :email, `subject` = :subject, `body` = :body, `body_nomarkup` = :body_nomarkup WHERE `id` = :id', $board));
1470 else
1471 $query = prepare(sprintf('UPDATE ``posts_%s`` SET `name` = :name, `email` = :email, `subject` = :subject, `body_nomarkup` = :body WHERE `id` = :id', $board));
1472 $query->bindValue(':id', $postID);
1473 $query->bindValue('name', $_POST['name']);
1474 $query->bindValue(':email', $_POST['email']);
1475 $query->bindValue(':subject', $_POST['subject']);
1476 $query->bindValue(':body', $_POST['body']);
1477 if ($edit_raw_html) {
1478 $body_nomarkup = $_POST['body'] . "\n<tinyboard raw html>1</tinyboard>";
1479 $query->bindValue(':body_nomarkup', $body_nomarkup);
1480 }
1481 $query->execute() or error(db_error($query));
1482
1483 if ($edit_raw_html) {
1484 modLog("Edited raw HTML of post #{$postID}");
1485 } else {
1486 modLog("Edited post #{$postID}");
1487 rebuildPost($postID);
1488 }
1489
1490 buildIndex();
1491
1492 rebuildThemes('post', $board);
1493
1494 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $postID) . '#' . $postID, true, $config['redirect_http']);
1495 } else {
1496 if ($config['minify_html']) {
1497 $post['body_nomarkup'] = str_replace("\n", '
', utf8tohtml($post['body_nomarkup']));
1498 $post['body'] = str_replace("\n", '
', utf8tohtml($post['body']));
1499 $post['body_nomarkup'] = str_replace("\r", '', $post['body_nomarkup']);
1500 $post['body'] = str_replace("\r", '', $post['body']);
1501 $post['body_nomarkup'] = str_replace("\t", '	', $post['body_nomarkup']);
1502 $post['body'] = str_replace("\t", '	', $post['body']);
1503 }
1504
1505 mod_page(_('Edit post'), 'mod/edit_post_form.html', array('token' => $security_token, 'board' => $board, 'raw' => $edit_raw_html, 'post' => $post));
1506 }
1507}
1508
1509function mod_delete($board, $post) {
1510 global $config, $mod;
1511
1512 if (!openBoard($board))
1513 error($config['error']['noboard']);
1514
1515 if (!hasPermission($config['mod']['delete'], $board))
1516 error($config['error']['noaccess']);
1517
1518 // Delete post
1519 deletePost($post);
1520 // Record the action
1521 modLog("Deleted post #{$post}");
1522 // Rebuild board
1523 buildIndex();
1524 // Rebuild themes
1525 rebuildThemes('post-delete', $board);
1526 // Redirect
1527 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1528}
1529
1530function mod_deletefile($board, $post, $file) {
1531 global $config, $mod;
1532
1533 if (!openBoard($board))
1534 error($config['error']['noboard']);
1535
1536 if (!hasPermission($config['mod']['deletefile'], $board))
1537 error($config['error']['noaccess']);
1538
1539 // Delete file
1540 deleteFile($post, TRUE, $file);
1541 // Record the action
1542 modLog("Deleted file from post #{$post}");
1543
1544 // Rebuild board
1545 buildIndex();
1546 // Rebuild themes
1547 rebuildThemes('post-delete', $board);
1548
1549 // Redirect
1550 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1551}
1552
1553function mod_spoiler_image($board, $post, $file) {
1554 global $config, $mod;
1555
1556 if (!openBoard($board))
1557 error($config['error']['noboard']);
1558
1559 if (!hasPermission($config['mod']['spoilerimage'], $board))
1560 error($config['error']['noaccess']);
1561
1562 // Delete file thumbnail
1563 $query = prepare(sprintf("SELECT `files`, `thread` FROM ``posts_%s`` WHERE id = :id", $board));
1564 $query->bindValue(':id', $post, PDO::PARAM_INT);
1565 $query->execute() or error(db_error($query));
1566 $result = $query->fetch(PDO::FETCH_ASSOC);
1567 $files = json_decode($result['files']);
1568
1569 file_unlink($board . '/' . $config['dir']['thumb'] . $files[$file]->thumb);
1570 $files[$file]->thumb = 'spoiler';
1571 $files[$file]->thumbheight = 128;
1572 $files[$file]->thumbwidth = 128;
1573
1574 // Make thumbnail spoiler
1575 $query = prepare(sprintf("UPDATE ``posts_%s`` SET `files` = :files WHERE `id` = :id", $board));
1576 $query->bindValue(':files', json_encode($files));
1577 $query->bindValue(':id', $post, PDO::PARAM_INT);
1578 $query->execute() or error(db_error($query));
1579
1580 // Record the action
1581 modLog("Spoilered file from post #{$post}");
1582
1583 // Rebuild thread
1584 buildThread($result['thread'] ? $result['thread'] : $post);
1585
1586 // Rebuild board
1587 buildIndex();
1588
1589 // Rebuild themes
1590 rebuildThemes('post-delete', $board);
1591
1592 // Redirect
1593 header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
1594}
1595
1596function mod_deletebyip($boardName, $post, $global = false) {
1597 global $config, $mod, $board;
1598
1599 $global = (bool)$global;
1600
1601 if (!openBoard($boardName))
1602 error($config['error']['noboard']);
1603
1604 if (!$global && !hasPermission($config['mod']['deletebyip'], $boardName))
1605 error($config['error']['noaccess']);
1606
1607 if ($global && !hasPermission($config['mod']['deletebyip_global'], $boardName))
1608 error($config['error']['noaccess']);
1609
1610 // Find IP address
1611 $query = prepare(sprintf('SELECT `ip` FROM ``posts_%s`` WHERE `id` = :id', $boardName));
1612 $query->bindValue(':id', $post);
1613 $query->execute() or error(db_error($query));
1614 if (!$ip = $query->fetchColumn())
1615 error($config['error']['invalidpost']);
1616
1617 $boards = $global ? listBoards() : array(array('uri' => $boardName));
1618
1619 $query = '';
1620 foreach ($boards as $_board) {
1621 $query .= sprintf("SELECT `thread`, `id`, '%s' AS `board` FROM ``posts_%s`` WHERE `ip` = :ip UNION ALL ", $_board['uri'], $_board['uri']);
1622 }
1623 $query = preg_replace('/UNION ALL $/', '', $query);
1624
1625 $query = prepare($query);
1626 $query->bindValue(':ip', $ip);
1627 $query->execute() or error(db_error($query));
1628
1629 if ($query->rowCount() < 1)
1630 error($config['error']['invalidpost']);
1631
1632 @set_time_limit($config['mod']['rebuild_timelimit']);
1633
1634 $threads_to_rebuild = array();
1635 $threads_deleted = array();
1636 while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
1637 openBoard($post['board']);
1638
1639 deletePost($post['id'], false, false);
1640
1641 rebuildThemes('post-delete', $board['uri']);
1642
1643 if ($post['thread'])
1644 $threads_to_rebuild[$post['board']][$post['thread']] = true;
1645 else
1646 $threads_deleted[$post['board']][$post['id']] = true;
1647 }
1648
1649 foreach ($threads_to_rebuild as $_board => $_threads) {
1650 openBoard($_board);
1651 foreach ($_threads as $_thread => $_dummy) {
1652 if ($_dummy && !isset($threads_deleted[$_board][$_thread]))
1653 buildThread($_thread);
1654 }
1655 buildIndex();
1656 }
1657
1658 if ($global) {
1659 $board = false;
1660 }
1661
1662 // Record the action
1663 modLog("Deleted all posts by IP address: <a href=\"?/IP/$ip\">$ip</a>");
1664
1665 // Redirect
1666 header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
1667}
1668
1669function mod_user($uid) {
1670 global $config, $mod;
1671
1672 if (!hasPermission($config['mod']['editusers']) && !(hasPermission($config['mod']['change_password']) && $uid == $mod['id']))
1673 error($config['error']['noaccess']);
1674
1675 $query = prepare('SELECT * FROM ``mods`` WHERE `id` = :id');
1676 $query->bindValue(':id', $uid);
1677 $query->execute() or error(db_error($query));
1678 if (!$user = $query->fetch(PDO::FETCH_ASSOC))
1679 error($config['error']['404']);
1680
1681 if (hasPermission($config['mod']['editusers']) && isset($_POST['username'], $_POST['password'])) {
1682 if (isset($_POST['allboards'])) {
1683 $boards = array('*');
1684 } else {
1685 $_boards = listBoards();
1686 foreach ($_boards as &$board) {
1687 $board = $board['uri'];
1688 }
1689
1690 $boards = array();
1691 foreach ($_POST as $name => $value) {
1692 if (preg_match('/^board_(' . $config['board_regex'] . ')$/u', $name, $matches) && in_array($matches[1], $_boards))
1693 $boards[] = $matches[1];
1694 }
1695 }
1696
1697 if (isset($_POST['delete'])) {
1698 if (!hasPermission($config['mod']['deleteusers']))
1699 error($config['error']['noaccess']);
1700
1701 $query = prepare('DELETE FROM ``mods`` WHERE `id` = :id');
1702 $query->bindValue(':id', $uid);
1703 $query->execute() or error(db_error($query));
1704
1705 modLog('Deleted user ' . utf8tohtml($user['username']) . ' <small>(#' . $user['id'] . ')</small>');
1706
1707 header('Location: ?/users', true, $config['redirect_http']);
1708
1709 return;
1710 }
1711
1712 if ($_POST['username'] == '')
1713 error(sprintf($config['error']['required'], 'username'));
1714
1715 $query = prepare('UPDATE ``mods`` SET `username` = :username, `boards` = :boards WHERE `id` = :id');
1716 $query->bindValue(':id', $uid);
1717 $query->bindValue(':username', $_POST['username']);
1718 $query->bindValue(':boards', implode(',', $boards));
1719 $query->execute() or error(db_error($query));
1720
1721 if ($user['username'] !== $_POST['username']) {
1722 // account was renamed
1723 modLog('Renamed user "' . utf8tohtml($user['username']) . '" <small>(#' . $user['id'] . ')</small> to "' . utf8tohtml($_POST['username']) . '"');
1724 }
1725
1726 if ($_POST['password'] != '') {
1727 $salt = generate_salt();
1728 $password = hash('sha256', $salt . sha1($_POST['password']));
1729
1730 $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id');
1731 $query->bindValue(':id', $uid);
1732 $query->bindValue(':password', $password);
1733 $query->bindValue(':salt', $salt);
1734 $query->execute() or error(db_error($query));
1735
1736 modLog('Changed password for ' . utf8tohtml($_POST['username']) . ' <small>(#' . $user['id'] . ')</small>');
1737
1738 if ($uid == $mod['id']) {
1739 login($_POST['username'], $_POST['password']);
1740 setCookies();
1741 }
1742 }
1743
1744 if (hasPermission($config['mod']['manageusers']))
1745 header('Location: ?/users', true, $config['redirect_http']);
1746 else
1747 header('Location: ?/', true, $config['redirect_http']);
1748
1749 return;
1750 }
1751
1752 if (hasPermission($config['mod']['change_password']) && $uid == $mod['id'] && isset($_POST['password'])) {
1753 if ($_POST['password'] != '') {
1754 $salt = generate_salt();
1755 $password = hash('sha256', $salt . sha1($_POST['password']));
1756
1757 $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id');
1758 $query->bindValue(':id', $uid);
1759 $query->bindValue(':password', $password);
1760 $query->bindValue(':salt', $salt);
1761 $query->execute() or error(db_error($query));
1762
1763 modLog('Changed own password');
1764
1765 login($user['username'], $_POST['password']);
1766 setCookies();
1767 }
1768
1769 if (hasPermission($config['mod']['manageusers']))
1770 header('Location: ?/users', true, $config['redirect_http']);
1771 else
1772 header('Location: ?/', true, $config['redirect_http']);
1773
1774 return;
1775 }
1776
1777 if (hasPermission($config['mod']['modlog'])) {
1778 $query = prepare('SELECT * FROM ``modlogs`` WHERE `mod` = :id ORDER BY `time` DESC LIMIT 5');
1779 $query->bindValue(':id', $uid);
1780 $query->execute() or error(db_error($query));
1781 $log = $query->fetchAll(PDO::FETCH_ASSOC);
1782 } else {
1783 $log = array();
1784 }
1785
1786 $user['boards'] = explode(',', $user['boards']);
1787
1788 mod_page(_('Edit user'), 'mod/user.html', array(
1789 'user' => $user,
1790 'logs' => $log,
1791 'boards' => listBoards(),
1792 'token' => make_secure_link_token('users/' . $user['id'])
1793 ));
1794}
1795
1796function mod_user_new() {
1797 global $pdo, $config;
1798
1799 if (!hasPermission($config['mod']['createusers']))
1800 error($config['error']['noaccess']);
1801
1802 if (isset($_POST['username'], $_POST['password'], $_POST['type'])) {
1803 if ($_POST['username'] == '')
1804 error(sprintf($config['error']['required'], 'username'));
1805 if ($_POST['password'] == '')
1806 error(sprintf($config['error']['required'], 'password'));
1807
1808 if (isset($_POST['allboards'])) {
1809 $boards = array('*');
1810 } else {
1811 $_boards = listBoards();
1812 foreach ($_boards as &$board) {
1813 $board = $board['uri'];
1814 }
1815
1816 $boards = array();
1817 foreach ($_POST as $name => $value) {
1818 if (preg_match('/^board_(' . $config['board_regex'] . ')$/u', $name, $matches) && in_array($matches[1], $_boards))
1819 $boards[] = $matches[1];
1820 }
1821 }
1822
1823 $type = (int)$_POST['type'];
1824 if (!isset($config['mod']['groups'][$type]) || $type == DISABLED)
1825 error(sprintf($config['error']['invalidfield'], 'type'));
1826
1827 $salt = generate_salt();
1828 $password = hash('sha256', $salt . sha1($_POST['password']));
1829
1830 $query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :salt, :type, :boards)');
1831 $query->bindValue(':username', $_POST['username']);
1832 $query->bindValue(':password', $password);
1833 $query->bindValue(':salt', $salt);
1834 $query->bindValue(':type', $type);
1835 $query->bindValue(':boards', implode(',', $boards));
1836 $query->execute() or error(db_error($query));
1837
1838 $userID = $pdo->lastInsertId();
1839
1840 modLog('Created a new user: ' . utf8tohtml($_POST['username']) . ' <small>(#' . $userID . ')</small>');
1841
1842 header('Location: ?/users', true, $config['redirect_http']);
1843 return;
1844 }
1845
1846 mod_page(_('New user'), 'mod/user.html', array('new' => true, 'boards' => listBoards(), 'token' => make_secure_link_token('users/new')));
1847}
1848
1849
1850function mod_users() {
1851 global $config;
1852
1853 if (!hasPermission($config['mod']['manageusers']))
1854 error($config['error']['noaccess']);
1855
1856 $query = query("SELECT
1857 *,
1858 (SELECT `time` FROM ``modlogs`` WHERE `mod` = `id` ORDER BY `time` DESC LIMIT 1) AS `last`,
1859 (SELECT `text` FROM ``modlogs`` WHERE `mod` = `id` ORDER BY `time` DESC LIMIT 1) AS `action`
1860 FROM ``mods`` ORDER BY `type` DESC,`id`") or error(db_error());
1861 $users = $query->fetchAll(PDO::FETCH_ASSOC);
1862
1863 foreach ($users as &$user) {
1864 $user['promote_token'] = make_secure_link_token("users/{$user['id']}/promote");
1865 $user['demote_token'] = make_secure_link_token("users/{$user['id']}/demote");
1866 }
1867
1868 mod_page(sprintf('%s (%d)', _('Manage users'), count($users)), 'mod/users.html', array('users' => $users));
1869}
1870
1871function mod_user_promote($uid, $action) {
1872 global $config;
1873
1874 if (!hasPermission($config['mod']['promoteusers']))
1875 error($config['error']['noaccess']);
1876
1877 $query = prepare("SELECT `type`, `username` FROM ``mods`` WHERE `id` = :id");
1878 $query->bindValue(':id', $uid);
1879 $query->execute() or error(db_error($query));
1880
1881 if (!$mod = $query->fetch(PDO::FETCH_ASSOC))
1882 error($config['error']['404']);
1883
1884 $new_group = false;
1885
1886 $groups = $config['mod']['groups'];
1887 if ($action == 'demote')
1888 $groups = array_reverse($groups, true);
1889
1890 foreach ($groups as $group_value => $group_name) {
1891 if ($action == 'promote' && $group_value > $mod['type']) {
1892 $new_group = $group_value;
1893 break;
1894 } elseif ($action == 'demote' && $group_value < $mod['type']) {
1895 $new_group = $group_value;
1896 break;
1897 }
1898 }
1899
1900 if ($new_group === false || $new_group == DISABLED)
1901 error(_('Impossible to promote/demote user.'));
1902
1903 $query = prepare("UPDATE ``mods`` SET `type` = :group_value WHERE `id` = :id");
1904 $query->bindValue(':id', $uid);
1905 $query->bindValue(':group_value', $new_group);
1906 $query->execute() or error(db_error($query));
1907
1908 modLog(($action == 'promote' ? 'Promoted' : 'Demoted') . ' user "' .
1909 utf8tohtml($mod['username']) . '" to ' . $config['mod']['groups'][$new_group]);
1910
1911 header('Location: ?/users', true, $config['redirect_http']);
1912}
1913
1914function mod_pm($id, $reply = false) {
1915 global $mod, $config;
1916
1917 if ($reply && !hasPermission($config['mod']['create_pm']))
1918 error($config['error']['noaccess']);
1919
1920 $query = prepare("SELECT ``mods``.`username`, `mods_to`.`username` AS `to_username`, ``pms``.* FROM ``pms`` LEFT JOIN ``mods`` ON ``mods``.`id` = `sender` LEFT JOIN ``mods`` AS `mods_to` ON `mods_to`.`id` = `to` WHERE ``pms``.`id` = :id");
1921 $query->bindValue(':id', $id);
1922 $query->execute() or error(db_error($query));
1923
1924 if ((!$pm = $query->fetch(PDO::FETCH_ASSOC)) || ($pm['to'] != $mod['id'] && !hasPermission($config['mod']['master_pm'])))
1925 error($config['error']['404']);
1926
1927 if (isset($_POST['delete'])) {
1928 $query = prepare("DELETE FROM ``pms`` WHERE `id` = :id");
1929 $query->bindValue(':id', $id);
1930 $query->execute() or error(db_error($query));
1931
1932 if ($config['cache']['enabled']) {
1933 cache::delete('pm_unread_' . $mod['id']);
1934 cache::delete('pm_unreadcount_' . $mod['id']);
1935 }
1936
1937 header('Location: ?/', true, $config['redirect_http']);
1938 return;
1939 }
1940
1941 if ($pm['unread'] && $pm['to'] == $mod['id']) {
1942 $query = prepare("UPDATE ``pms`` SET `unread` = 0 WHERE `id` = :id");
1943 $query->bindValue(':id', $id);
1944 $query->execute() or error(db_error($query));
1945
1946 if ($config['cache']['enabled']) {
1947 cache::delete('pm_unread_' . $mod['id']);
1948 cache::delete('pm_unreadcount_' . $mod['id']);
1949 }
1950
1951 modLog('Read a PM');
1952 }
1953
1954 if ($reply) {
1955 if (!$pm['to_username'])
1956 error($config['error']['404']); // deleted?
1957
1958 mod_page(sprintf('%s %s', _('New PM for'), $pm['to_username']), 'mod/new_pm.html', array(
1959 'username' => $pm['username'],
1960 'id' => $pm['sender'],
1961 'message' => quote($pm['message']),
1962 'token' => make_secure_link_token('new_PM/' . $pm['username'])
1963 ));
1964 } else {
1965 mod_page(sprintf('%s – #%d', _('Private message'), $id), 'mod/pm.html', $pm);
1966 }
1967}
1968
1969function mod_inbox() {
1970 global $config, $mod;
1971
1972 $query = prepare('SELECT `unread`,``pms``.`id`, `time`, `sender`, `to`, `message`, `username` FROM ``pms`` LEFT JOIN ``mods`` ON ``mods``.`id` = `sender` WHERE `to` = :mod ORDER BY `unread` DESC, `time` DESC');
1973 $query->bindValue(':mod', $mod['id']);
1974 $query->execute() or error(db_error($query));
1975 $messages = $query->fetchAll(PDO::FETCH_ASSOC);
1976
1977 $query = prepare('SELECT COUNT(*) FROM ``pms`` WHERE `to` = :mod AND `unread` = 1');
1978 $query->bindValue(':mod', $mod['id']);
1979 $query->execute() or error(db_error($query));
1980 $unread = $query->fetchColumn();
1981
1982 foreach ($messages as &$message) {
1983 $message['snippet'] = pm_snippet($message['message']);
1984 }
1985
1986 mod_page(sprintf('%s (%s)', _('PM inbox'), count($messages) > 0 ? $unread . ' unread' : 'empty'), 'mod/inbox.html', array(
1987 'messages' => $messages,
1988 'unread' => $unread
1989 ));
1990}
1991
1992
1993function mod_new_pm($username) {
1994 global $config, $mod;
1995
1996 if (!hasPermission($config['mod']['create_pm']))
1997 error($config['error']['noaccess']);
1998
1999 $query = prepare("SELECT `id` FROM ``mods`` WHERE `username` = :username");
2000 $query->bindValue(':username', $username);
2001 $query->execute() or error(db_error($query));
2002 if (!$id = $query->fetchColumn()) {
2003 // Old style ?/PM: by user ID
2004 $query = prepare("SELECT `username` FROM ``mods`` WHERE `id` = :username");
2005 $query->bindValue(':username', $username);
2006 $query->execute() or error(db_error($query));
2007 if ($username = $query->fetchColumn())
2008 header('Location: ?/new_PM/' . $username, true, $config['redirect_http']);
2009 else
2010 error($config['error']['404']);
2011 }
2012
2013 if (isset($_POST['message'])) {
2014 $_POST['message'] = escape_markup_modifiers($_POST['message']);
2015 markup($_POST['message']);
2016
2017 $query = prepare("INSERT INTO ``pms`` VALUES (NULL, :me, :id, :message, :time, 1)");
2018 $query->bindValue(':me', $mod['id']);
2019 $query->bindValue(':id', $id);
2020 $query->bindValue(':message', $_POST['message']);
2021 $query->bindValue(':time', time());
2022 $query->execute() or error(db_error($query));
2023
2024 if ($config['cache']['enabled']) {
2025 cache::delete('pm_unread_' . $id);
2026 cache::delete('pm_unreadcount_' . $id);
2027 }
2028
2029 modLog('Sent a PM to ' . utf8tohtml($username));
2030
2031 header('Location: ?/', true, $config['redirect_http']);
2032 }
2033
2034 mod_page(sprintf('%s %s', _('New PM for'), $username), 'mod/new_pm.html', array(
2035 'username' => $username,
2036 'id' => $id,
2037 'token' => make_secure_link_token('new_PM/' . $username)
2038 ));
2039}
2040
2041function mod_rebuild() {
2042 global $config, $twig;
2043
2044 if (!hasPermission($config['mod']['rebuild']))
2045 error($config['error']['noaccess']);
2046
2047 if (isset($_POST['rebuild'])) {
2048 @set_time_limit($config['mod']['rebuild_timelimit']);
2049
2050 $log = array();
2051 $boards = listBoards();
2052 $rebuilt_scripts = array();
2053
2054 if (isset($_POST['rebuild_cache'])) {
2055 if ($config['cache']['enabled']) {
2056 $log[] = 'Flushing cache';
2057 Cache::flush();
2058 }
2059
2060 $log[] = 'Clearing template cache';
2061 load_twig();
2062 $twig->clearCacheFiles();
2063 }
2064
2065 if (isset($_POST['rebuild_themes'])) {
2066 $log[] = 'Regenerating theme files';
2067 rebuildThemes('all');
2068 }
2069
2070 if (isset($_POST['rebuild_javascript'])) {
2071 $log[] = 'Rebuilding <strong>' . $config['file_script'] . '</strong>';
2072 buildJavascript();
2073 $rebuilt_scripts[] = $config['file_script'];
2074 }
2075
2076 foreach ($boards as $board) {
2077 if (!(isset($_POST['boards_all']) || isset($_POST['board_' . $board['uri']])))
2078 continue;
2079
2080 openBoard($board['uri']);
2081 $config['try_smarter'] = false;
2082
2083 if (isset($_POST['rebuild_index'])) {
2084 buildIndex();
2085 $log[] = '<strong>' . sprintf($config['board_abbreviation'], $board['uri']) . '</strong>: Creating index pages';
2086 }
2087
2088 if (isset($_POST['rebuild_javascript']) && !in_array($config['file_script'], $rebuilt_scripts)) {
2089 $log[] = '<strong>' . sprintf($config['board_abbreviation'], $board['uri']) . '</strong>: Rebuilding <strong>' . $config['file_script'] . '</strong>';
2090 buildJavascript();
2091 $rebuilt_scripts[] = $config['file_script'];
2092 }
2093
2094 if (isset($_POST['rebuild_thread'])) {
2095 $query = query(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `thread` IS NULL", $board['uri'])) or error(db_error());
2096 while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
2097 $log[] = '<strong>' . sprintf($config['board_abbreviation'], $board['uri']) . '</strong>: Rebuilding thread #' . $post['id'];
2098 buildThread($post['id']);
2099 }
2100 }
2101 }
2102
2103 mod_page(_('Rebuild'), 'mod/rebuilt.html', array('logs' => $log));
2104 return;
2105 }
2106
2107 mod_page(_('Rebuild'), 'mod/rebuild.html', array(
2108 'boards' => listBoards(),
2109 'token' => make_secure_link_token('rebuild')
2110 ));
2111}
2112
2113function mod_reports() {
2114 global $config, $mod;
2115
2116 if (!hasPermission($config['mod']['reports']))
2117 error($config['error']['noaccess']);
2118
2119 $query = prepare("SELECT * FROM ``reports`` ORDER BY `time` DESC LIMIT :limit");
2120 $query->bindValue(':limit', $config['mod']['recent_reports'], PDO::PARAM_INT);
2121 $query->execute() or error(db_error($query));
2122 $reports = $query->fetchAll(PDO::FETCH_ASSOC);
2123
2124 $report_queries = array();
2125 foreach ($reports as $report) {
2126 if (!isset($report_queries[$report['board']]))
2127 $report_queries[$report['board']] = array();
2128 $report_queries[$report['board']][] = $report['post'];
2129 }
2130
2131 $report_posts = array();
2132 foreach ($report_queries as $board => $posts) {
2133 $report_posts[$board] = array();
2134
2135 $query = query(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $posts), $board)) or error(db_error());
2136 while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
2137 $report_posts[$board][$post['id']] = $post;
2138 }
2139 }
2140
2141 $count = 0;
2142 $body = '';
2143 foreach ($reports as $report) {
2144 if (!isset($report_posts[$report['board']][$report['post']])) {
2145 // // Invalid report (post has since been deleted)
2146 $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board");
2147 $query->bindValue(':id', $report['post'], PDO::PARAM_INT);
2148 $query->bindValue(':board', $report['board']);
2149 $query->execute() or error(db_error($query));
2150 continue;
2151 }
2152
2153 openBoard($report['board']);
2154
2155 $post = &$report_posts[$report['board']][$report['post']];
2156
2157 if (!$post['thread']) {
2158 // Still need to fix this:
2159 $po = new Thread($post, '?/', $mod, false);
2160 } else {
2161 $po = new Post($post, '?/', $mod);
2162 }
2163
2164 // a little messy and inefficient
2165 $append_html = Element('mod/report.html', array(
2166 'report' => $report,
2167 'config' => $config,
2168 'mod' => $mod,
2169 'token' => make_secure_link_token('reports/' . $report['id'] . '/dismiss'),
2170 'token_all' => make_secure_link_token('reports/' . $report['id'] . '/dismissall')
2171 ));
2172
2173 // Bug fix for https://github.com/savetheinternet/Tinyboard/issues/21
2174 $po->body = truncate($po->body, $po->link(), $config['body_truncate'] - substr_count($append_html, '<br>'));
2175
2176 if (mb_strlen($po->body) + mb_strlen($append_html) > $config['body_truncate_char']) {
2177 // still too long; temporarily increase limit in the config
2178 $__old_body_truncate_char = $config['body_truncate_char'];
2179 $config['body_truncate_char'] = mb_strlen($po->body) + mb_strlen($append_html);
2180 }
2181
2182 $po->body .= $append_html;
2183
2184 $body .= $po->build(true) . '<hr>';
2185
2186 if (isset($__old_body_truncate_char))
2187 $config['body_truncate_char'] = $__old_body_truncate_char;
2188
2189 $count++;
2190 }
2191
2192 mod_page(sprintf('%s (%d)', _('Report queue'), $count), 'mod/reports.html', array('reports' => $body, 'count' => $count));
2193}
2194
2195function mod_report_dismiss($id, $all = false) {
2196 global $config;
2197
2198 $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id");
2199 $query->bindValue(':id', $id);
2200 $query->execute() or error(db_error($query));
2201 if ($report = $query->fetch(PDO::FETCH_ASSOC)) {
2202 $ip = $report['ip'];
2203 $board = $report['board'];
2204 $post = $report['post'];
2205 } else
2206 error($config['error']['404']);
2207
2208 if (!$all && !hasPermission($config['mod']['report_dismiss'], $board))
2209 error($config['error']['noaccess']);
2210
2211 if ($all && !hasPermission($config['mod']['report_dismiss_ip'], $board))
2212 error($config['error']['noaccess']);
2213
2214 if ($all) {
2215 $query = prepare("DELETE FROM ``reports`` WHERE `ip` = :ip");
2216 $query->bindValue(':ip', $ip);
2217 } else {
2218 $query = prepare("DELETE FROM ``reports`` WHERE `id` = :id");
2219 $query->bindValue(':id', $id);
2220 }
2221 $query->execute() or error(db_error($query));
2222
2223
2224 if ($all)
2225 modLog("Dismissed all reports by <a href=\"?/IP/$ip\">$ip</a>");
2226 else
2227 modLog("Dismissed a report for post #{$id}", $board);
2228
2229 header('Location: ?/reports', true, $config['redirect_http']);
2230}
2231
2232function mod_recent_posts($lim) {
2233 global $config, $mod, $pdo;
2234
2235 if (!hasPermission($config['mod']['recent']))
2236 error($config['error']['noaccess']);
2237
2238 $limit = (is_numeric($lim))? $lim : 25;
2239 $last_time = (isset($_GET['last']) && is_numeric($_GET['last'])) ? $_GET['last'] : 0;
2240
2241 $mod_boards = array();
2242 $boards = listBoards();
2243
2244 //if not all boards
2245 if ($mod['boards'][0]!='*') {
2246 foreach ($boards as $board) {
2247 if (in_array($board['uri'], $mod['boards']))
2248 $mod_boards[] = $board;
2249 }
2250 } else {
2251 $mod_boards = $boards;
2252 }
2253
2254 // Manually build an SQL query
2255 $query = 'SELECT * FROM (';
2256 foreach ($mod_boards as $board) {
2257 $query .= sprintf('SELECT *, %s AS `board` FROM ``posts_%s`` UNION ALL ', $pdo->quote($board['uri']), $board['uri']);
2258 }
2259 // Remove the last "UNION ALL" seperator and complete the query
2260 $query = preg_replace('/UNION ALL $/', ') AS `all_posts` WHERE (`time` < :last_time OR NOT :last_time) ORDER BY `time` DESC LIMIT ' . $limit, $query);
2261 $query = prepare($query);
2262 $query->bindValue(':last_time', $last_time);
2263 $query->execute() or error(db_error($query));
2264 $posts = $query->fetchAll(PDO::FETCH_ASSOC);
2265
2266 foreach ($posts as &$post) {
2267 openBoard($post['board']);
2268 if (!$post['thread']) {
2269 // Still need to fix this:
2270 $po = new Thread($post, '?/', $mod, false);
2271 $post['built'] = $po->build(true);
2272 } else {
2273 $po = new Post($post, '?/', $mod);
2274 $post['built'] = $po->build(true);
2275 }
2276 $last_time = $post['time'];
2277 }
2278
2279 echo mod_page(_('Recent posts'), 'mod/recent_posts.html', array(
2280 'posts' => $posts,
2281 'limit' => $limit,
2282 'last_time' => $last_time
2283 )
2284 );
2285
2286}
2287
2288function mod_config($board_config = false) {
2289 global $config, $mod, $board;
2290
2291 if ($board_config && !openBoard($board_config))
2292 error($config['error']['noboard']);
2293
2294 if (!hasPermission($config['mod']['edit_config'], $board_config))
2295 error($config['error']['noaccess']);
2296
2297 $config_file = $board_config ? $board['dir'] . 'config.php' : 'inc/instance-config.php';
2298
2299 if ($config['mod']['config_editor_php']) {
2300 $readonly = !(is_file($config_file) ? is_writable($config_file) : is_writable(dirname($config_file)));
2301
2302 if (!$readonly && isset($_POST['code'])) {
2303 $code = $_POST['code'];
2304 // Save previous instance_config if php_check_syntax fails
2305 $old_code = file_get_contents($config_file);
2306 file_put_contents($config_file, $code);
2307 $resp = shell_exec_error('php -l ' . $config_file);
2308 if (preg_match('/No syntax errors detected/', $resp)) {
2309 header('Location: ?/config' . ($board_config ? '/' . $board_config : ''), true, $config['redirect_http']);
2310 return;
2311 }
2312 else {
2313 file_put_contents($config_file, $old_code);
2314 error($config['error']['badsyntax'] . $resp);
2315 }
2316 }
2317
2318 $instance_config = @file_get_contents($config_file);
2319 if ($instance_config === false) {
2320 $instance_config = "<?php\n\n// This file does not exist yet. You are creating it.";
2321 }
2322 $instance_config = str_replace("\n", '
', utf8tohtml($instance_config));
2323
2324 mod_page(_('Config editor'), 'mod/config-editor-php.html', array(
2325 'php' => $instance_config,
2326 'readonly' => $readonly,
2327 'boards' => listBoards(),
2328 'board' => $board_config,
2329 'file' => $config_file,
2330 'token' => make_secure_link_token('config' . ($board_config ? '/' . $board_config : ''))
2331 ));
2332 return;
2333 }
2334
2335 require_once 'inc/mod/config-editor.php';
2336
2337 $conf = config_vars();
2338
2339 foreach ($conf as &$var) {
2340 if (is_array($var['name'])) {
2341 $c = &$config;
2342 foreach ($var['name'] as $n)
2343 $c = &$c[$n];
2344 } else {
2345 $c = @$config[$var['name']];
2346 }
2347
2348 $var['value'] = $c;
2349 }
2350 unset($var);
2351
2352 if (isset($_POST['save'])) {
2353 $config_append = '';
2354
2355 foreach ($conf as $var) {
2356 $field_name = 'cf_' . (is_array($var['name']) ? implode('/', $var['name']) : $var['name']);
2357
2358 if ($var['type'] == 'boolean')
2359 $value = isset($_POST[$field_name]);
2360 elseif (isset($_POST[$field_name]))
2361 $value = $_POST[$field_name];
2362 else
2363 continue; // ???
2364
2365 if (!settype($value, $var['type']))
2366 continue; // invalid
2367
2368 if ($value != $var['value']) {
2369 // This value has been changed.
2370
2371 $config_append .= '$config';
2372
2373 if (is_array($var['name'])) {
2374 foreach ($var['name'] as $name)
2375 $config_append .= '[' . var_export($name, true) . ']';
2376 } else {
2377 $config_append .= '[' . var_export($var['name'], true) . ']';
2378 }
2379
2380
2381 $config_append .= ' = ';
2382 if (@$var['permissions'] && isset($config['mod']['groups'][$value])) {
2383 $config_append .= $config['mod']['groups'][$value];
2384 } else {
2385 $config_append .= var_export($value, true);
2386 }
2387 $config_append .= ";\n";
2388 }
2389 }
2390
2391 if (!empty($config_append)) {
2392 $config_append = "\n// Changes made via web editor by \"" . $mod['username'] . "\" @ " . date('r') . ":\n" . $config_append . "\n";
2393 if (!is_file($config_file))
2394 $config_append = "<?php\n\n$config_append";
2395 if (!@file_put_contents($config_file, $config_append, FILE_APPEND)) {
2396 $config_append = htmlentities($config_append);
2397
2398 if ($config['minify_html'])
2399 $config_append = str_replace("\n", '
', $config_append);
2400 $page = array();
2401 $page['title'] = 'Cannot write to file!';
2402 $page['config'] = $config;
2403 $page['body'] = '
2404 <p style="text-align:center">Tinyboard could not write to <strong>' . $config_file . '</strong> with the ammended configuration, probably due to a permissions error.</p>
2405 <p style="text-align:center">You may proceed with these changes manually by copying and pasting the following code to the end of <strong>' . $config_file . '</strong>:</p>
2406 <textarea style="width:700px;height:370px;margin:auto;display:block;background:white;color:black" readonly>' . $config_append . '</textarea>
2407 ';
2408 echo Element('page.html', $page);
2409 exit;
2410 }
2411 }
2412
2413 header('Location: ?/config' . ($board_config ? '/' . $board_config : ''), true, $config['redirect_http']);
2414
2415 exit;
2416 }
2417
2418 mod_page(_('Config editor') . ($board_config ? ': ' . sprintf($config['board_abbreviation'], $board_config) : ''),
2419 'mod/config-editor.html', array(
2420 'boards' => listBoards(),
2421 'board' => $board_config,
2422 'conf' => $conf,
2423 'file' => $config_file,
2424 'token' => make_secure_link_token('config' . ($board_config ? '/' . $board_config : ''))
2425 ));
2426}
2427
2428function mod_themes_list() {
2429 global $config;
2430
2431 if (!hasPermission($config['mod']['themes']))
2432 error($config['error']['noaccess']);
2433
2434 if (!is_dir($config['dir']['themes']))
2435 error(_('Themes directory doesn\'t exist!'));
2436 if (!$dir = opendir($config['dir']['themes']))
2437 error(_('Cannot open themes directory; check permissions.'));
2438
2439 $query = query('SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL') or error(db_error());
2440 $themes_in_use = $query->fetchAll(PDO::FETCH_COLUMN);
2441
2442 // Scan directory for themes
2443 $themes = array();
2444 while ($file = readdir($dir)) {
2445 if ($file[0] != '.' && is_dir($config['dir']['themes'] . '/' . $file)) {
2446 $themes[$file] = loadThemeConfig($file);
2447 }
2448 }
2449 closedir($dir);
2450
2451 foreach ($themes as $theme_name => &$theme) {
2452 $theme['rebuild_token'] = make_secure_link_token('themes/' . $theme_name . '/rebuild');
2453 $theme['uninstall_token'] = make_secure_link_token('themes/' . $theme_name . '/uninstall');
2454 }
2455
2456 mod_page(_('Manage themes'), 'mod/themes.html', array(
2457 'themes' => $themes,
2458 'themes_in_use' => $themes_in_use,
2459 ));
2460}
2461
2462function mod_theme_configure($theme_name) {
2463 global $config;
2464
2465 if (!hasPermission($config['mod']['themes']))
2466 error($config['error']['noaccess']);
2467
2468 if (!$theme = loadThemeConfig($theme_name)) {
2469 error($config['error']['invalidtheme']);
2470 }
2471
2472 if (isset($_POST['install'])) {
2473 // Check if everything is submitted
2474 foreach ($theme['config'] as &$conf) {
2475 if (!isset($_POST[$conf['name']]) && $conf['type'] != 'checkbox')
2476 error(sprintf($config['error']['required'], $c['title']));
2477 }
2478
2479 // Clear previous settings
2480 $query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
2481 $query->bindValue(':theme', $theme_name);
2482 $query->execute() or error(db_error($query));
2483
2484 foreach ($theme['config'] as &$conf) {
2485 $query = prepare("INSERT INTO ``theme_settings`` VALUES(:theme, :name, :value)");
2486 $query->bindValue(':theme', $theme_name);
2487 $query->bindValue(':name', $conf['name']);
2488 if ($conf['type'] == 'checkbox')
2489 $query->bindValue(':value', isset($_POST[$conf['name']]) ? 1 : 0);
2490 else
2491 $query->bindValue(':value', $_POST[$conf['name']]);
2492 $query->execute() or error(db_error($query));
2493 }
2494
2495 $query = prepare("INSERT INTO ``theme_settings`` VALUES(:theme, NULL, NULL)");
2496 $query->bindValue(':theme', $theme_name);
2497 $query->execute() or error(db_error($query));
2498
2499 $result = true;
2500 $message = false;
2501 if (isset($theme['install_callback'])) {
2502 $ret = $theme['install_callback'](themeSettings($theme_name));
2503 if ($ret && !empty($ret)) {
2504 if (is_array($ret) && count($ret) == 2) {
2505 $result = $ret[0];
2506 $message = $ret[1];
2507 }
2508 }
2509 }
2510
2511 if (!$result) {
2512 // Install failed
2513 $query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
2514 $query->bindValue(':theme', $theme_name);
2515 $query->execute() or error(db_error($query));
2516 }
2517
2518 // Build themes
2519 rebuildThemes('all');
2520
2521 mod_page(sprintf(_($result ? 'Installed theme: %s' : 'Installation failed: %s'), $theme['name']), 'mod/theme_installed.html', array(
2522 'theme_name' => $theme_name,
2523 'theme' => $theme,
2524 'result' => $result,
2525 'message' => $message
2526 ));
2527 return;
2528 }
2529
2530 $settings = themeSettings($theme_name);
2531
2532 mod_page(sprintf(_('Configuring theme: %s'), $theme['name']), 'mod/theme_config.html', array(
2533 'theme_name' => $theme_name,
2534 'theme' => $theme,
2535 'settings' => $settings,
2536 'token' => make_secure_link_token('themes/' . $theme_name)
2537 ));
2538}
2539
2540function mod_theme_uninstall($theme_name) {
2541 global $config;
2542
2543 if (!hasPermission($config['mod']['themes']))
2544 error($config['error']['noaccess']);
2545
2546 $query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
2547 $query->bindValue(':theme', $theme_name);
2548 $query->execute() or error(db_error($query));
2549
2550 header('Location: ?/themes', true, $config['redirect_http']);
2551}
2552
2553function mod_theme_rebuild($theme_name) {
2554 global $config;
2555
2556 if (!hasPermission($config['mod']['themes']))
2557 error($config['error']['noaccess']);
2558
2559 rebuildTheme($theme_name, 'all');
2560
2561 mod_page(sprintf(_('Rebuilt theme: %s'), $theme_name), 'mod/theme_rebuilt.html', array(
2562 'theme_name' => $theme_name,
2563 ));
2564}
2565
2566function mod_debug_antispam() {
2567 global $pdo, $config;
2568
2569 $args = array();
2570
2571 if (isset($_POST['board'], $_POST['thread'])) {
2572 $where = '`board` = ' . $pdo->quote($_POST['board']);
2573 if ($_POST['thread'] != '')
2574 $where .= ' AND `thread` = ' . $pdo->quote($_POST['thread']);
2575
2576 if (isset($_POST['purge'])) {
2577 $query = prepare(', DATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE' . $where);
2578 $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
2579 $query->execute() or error(db_error());
2580 }
2581
2582 $args['board'] = $_POST['board'];
2583 $args['thread'] = $_POST['thread'];
2584 } else {
2585 $where = '';
2586 }
2587
2588 $query = query('SELECT COUNT(*) FROM ``antispam``' . ($where ? " WHERE $where" : '')) or error(db_error());
2589 $args['total'] = number_format($query->fetchColumn());
2590
2591 $query = query('SELECT COUNT(*) FROM ``antispam`` WHERE `expires` IS NOT NULL' . ($where ? " AND $where" : '')) or error(db_error());
2592 $args['expiring'] = number_format($query->fetchColumn());
2593
2594 $query = query('SELECT * FROM ``antispam`` ' . ($where ? "WHERE $where" : '') . ' ORDER BY `passed` DESC LIMIT 40') or error(db_error());
2595 $args['top'] = $query->fetchAll(PDO::FETCH_ASSOC);
2596
2597 $query = query('SELECT * FROM ``antispam`` ' . ($where ? "WHERE $where" : '') . ' ORDER BY `created` DESC LIMIT 20') or error(db_error());
2598 $args['recent'] = $query->fetchAll(PDO::FETCH_ASSOC);
2599
2600 mod_page(_('Debug: Anti-spam'), 'mod/debug/antispam.html', $args);
2601}
2602
2603function mod_debug_recent_posts() {
2604 global $pdo, $config;
2605
2606 $limit = 500;
2607
2608 $boards = listBoards();
2609
2610 // Manually build an SQL query
2611 $query = 'SELECT * FROM (';
2612 foreach ($boards as $board) {
2613 $query .= sprintf('SELECT *, %s AS `board` FROM ``posts_%s`` UNION ALL ', $pdo->quote($board['uri']), $board['uri']);
2614 }
2615 // Remove the last "UNION ALL" seperator and complete the query
2616 $query = preg_replace('/UNION ALL $/', ') AS `all_posts` ORDER BY `time` DESC LIMIT ' . $limit, $query);
2617 $query = query($query) or error(db_error());
2618 $posts = $query->fetchAll(PDO::FETCH_ASSOC);
2619
2620 // Fetch recent posts from flood prevention cache
2621 $query = query("SELECT * FROM ``flood`` ORDER BY `time` DESC") or error(db_error());
2622 $flood_posts = $query->fetchAll(PDO::FETCH_ASSOC);
2623
2624 foreach ($posts as &$post) {
2625 $post['snippet'] = pm_snippet($post['body']);
2626 foreach ($flood_posts as $flood_post) {
2627 if ($flood_post['time'] == $post['time'] &&
2628 $flood_post['posthash'] == make_comment_hex($post['body_nomarkup']) &&
2629 $flood_post['filehash'] == $post['filehash'])
2630 $post['in_flood_table'] = true;
2631 }
2632 }
2633
2634 mod_page(_('Debug: Recent posts'), 'mod/debug/recent_posts.html', array('posts' => $posts, 'flood_posts' => $flood_posts));
2635}
2636
2637function mod_debug_sql() {
2638 global $config;
2639
2640 if (!hasPermission($config['mod']['debug_sql']))
2641 error($config['error']['noaccess']);
2642
2643 $args['security_token'] = make_secure_link_token('debug/sql');
2644
2645 if (isset($_POST['query'])) {
2646 $args['query'] = $_POST['query'];
2647 if ($query = query($_POST['query'])) {
2648 $args['result'] = $query->fetchAll(PDO::FETCH_ASSOC);
2649 if (!empty($args['result']))
2650 $args['keys'] = array_keys($args['result'][0]);
2651 else
2652 $args['result'] = 'empty';
2653 } else {
2654 $args['error'] = db_error();
2655 }
2656 }
2657
2658 mod_page(_('Debug: SQL'), 'mod/debug/sql.html', $args);
2659}
2660
2661function mod_debug_apc() {
2662 global $config;
2663
2664 if (!hasPermission($config['mod']['debug_apc']))
2665 error($config['error']['noaccess']);
2666
2667 if ($config['cache']['enabled'] != 'apc')
2668 error('APC is not enabled.');
2669
2670 $cache_info = apc_cache_info('user');
2671
2672 // $cached_vars = new APCIterator('user', '/^' . $config['cache']['prefix'] . '/');
2673 $cached_vars = array();
2674 foreach ($cache_info['cache_list'] as $var) {
2675 if ($config['cache']['prefix'] != '' && strpos(isset($var['key']) ? $var['key'] : $var['info'], $config['cache']['prefix']) !== 0)
2676 continue;
2677 $cached_vars[] = $var;
2678 }
2679
2680 mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars));
2681}