diff --git a/composer.json b/composer.json index 0ba98be6..6272bf01 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "inc/lock.php", "inc/queue.php", "inc/polyfill.php", + "inc/announcements.php", "inc/functions.php" ] }, diff --git a/inc/announcements.php b/inc/announcements.php new file mode 100644 index 00000000..092d35de --- /dev/null +++ b/inc/announcements.php @@ -0,0 +1,155 @@ +bindValue(':mod', $mod_id); + $query->bindValue(':time', time()); + if ($announcement !== '') { + $announcement = escape_markup_modifiers($announcement); + markup($announcement); + $query->bindValue(':text', $announcement); + } else + error(sprintf($config['error']['required'], "Announcement")); + + $query->execute() or error(db_error($query)); + + modLog("Created a new announcement: " . utf8tohtml($announcement)); + self::buildAnnouncements(); + } + + static public function edit_announcement($id, $announcement) { + global $mod, $config; + + $query = prepare(sprintf("UPDATE ``announcements`` SET `text` = :text WHERE `id` = %d", (int)$id)); + if ($announcement !== '') { + $teannouncementxt = escape_markup_modifiers($announcement); + markup($announcement); + $query->bindValue(':text', $announcement); + } else + error(sprintf($config['error']['required'], "Announcement")); + + $query->execute() or error(db_error($query)); + + modLog("Edited announcement #" . $id . " - New Text: " . utf8tohtml($announcement)); + self::buildAnnouncements(); + } + + static public function delete_announcement($id) { + $query = prepare(sprintf("DELETE FROM ``announcements`` WHERE `id` = %d", (int)$id)); + $query->execute() or error(db_error($query)); + + modLog("Deleted announcement #" . $id); + self::buildAnnouncements(); + } + + static public function buildAnnouncements() { + self::buildShortAnnouncementTable(); + self::buildAnnouncementPages(); + } + + static public function buildShortAnnouncementTable() { + global $config; + + $count = $config['announcements']['show_count']; + $query = query("SELECT `text`,`date` FROM ``announcements`` ORDER BY `date` DESC" . (($count === false)?"":" LIMIT " . (int)$count)) or error(db_error($query)); + $announcements = $query->fetchAll(PDO::FETCH_ASSOC); + + foreach ($announcements as &$announce) { + $announce['date_formated'] = strftime($config['announcements']['date_format'], $announce['date']); + } + + $announcements_short = Element('announcements.html', array( + 'announcements' => $announcements, + )); + + file_write($config['dir']['home'] . "templates/generated/announcements_short.html", $announcements_short); + } + + + static public function buildAnnouncementPages() { + global $config; + + // Generate page for full list of announcements + if($config['announcements']['page']) + { + // Generate JSON file for full list of announcements + //file_write($config['dir']['home'] . "announcements.json", self::gen_public_json($config['announcements']['date_format'], false)); + + $query = query("SELECT ``announcements``.* FROM ``announcements`` + ORDER BY `date` DESC") or error(db_error($query)); + $announcements = $query->fetchAll(PDO::FETCH_ASSOC); + + foreach ($announcements as &$announce) { + $announce['date_formated'] = strftime($config['announcements']['date_format'], $announce['date']); + } + + // Generate page for full list of announcements + $announcement_page = Element('page.html', array( + 'config' => $config, + 'mod' => false, + 'hide_dashboard_link' => true, + 'boardlist' => createBoardList(false), + 'title' => _("Announcements"), + 'subtitle' => "", + 'nojavascript' => true, + 'body' => Element('announcements_list.html', array( + 'announcements' => $announcements, + 'mod' => false, + 'token_json' => false, + )) + )); + file_write($config['dir']['home'] . $config['announcements']['page_html'], $announcement_page); + } + } + +/* Might be used later for mobile API + static public function stream_json($out = false, $filter_staff = false, $date_format, $count = false) { + $query = query("SELECT ``announcements``.*, `username` FROM ``announcements`` + LEFT JOIN ``mods`` ON ``mods``.`id` = `creator` + ORDER BY `date` DESC" . (($count === false)?"":" LIMIT " . (int)$count)) or error(db_error($query)); + $announcements = $query->fetchAll(PDO::FETCH_ASSOC); + + $out ? fputs($out, "[") : print("["); + + // Last entry for json end check + $end = end($announcements); + + foreach ($announcements as &$announce) { + + if($filter_staff) + $announce['username'] = '?'; + + $announce['date_formated'] = strftime($date_format, $announce['date']); + + $json = json_encode($announce); + $out ? fputs($out, $json) : print($json); + + if ($announce['id'] != $end['id']) { + $out ? fputs($out, ",") : print(","); + } + } + + $out ? fputs($out, "]") : print("]"); + } + + // Returns json content to be written to json file. + static public function gen_public_json($date_format, $count = false) { + ob_start(); + self::stream_json(false, true, $date_format, $count); + $out = ob_get_contents(); + ob_end_clean(); + return $out; + } +*/ +}; + +?> \ No newline at end of file diff --git a/inc/config.php b/inc/config.php index 764bbda8..0a119462 100644 --- a/inc/config.php +++ b/inc/config.php @@ -946,6 +946,26 @@ // Allow unfiltered HTML in board subtitle. This is useful for placing icons and links. $config['allow_subtitle_html'] = false; +/* + * ==================== + * Announcements settings + * ==================== + */ + // Show small list of announcements. + $config['announcements']['show'] = true; + + // Number of announcements to include in short announcements lists. + $config['announcements']['show_count'] = 3; + + // Date format for announcements. + $config['announcements']['date_format'] = '%m/%d/%Y'; + + // Create full announcements page. + $config['announcements']['page'] = true; + + // Filename for file to hold complete list of announcements + $config['announcements']['page_html'] = "announcements.html"; + /* * ==================== * Display settings @@ -1676,6 +1696,8 @@ $config['mod']['public_ban'] = MOD; // Manage and install themes for homepage $config['mod']['themes'] = ADMIN; + // Create or delete announcements + $config['mod']['announcements'] = ADMIN; // Post news entries $config['mod']['news'] = ADMIN; // Custom name when posting news diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 28a6e4df..8e182e96 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -963,6 +963,41 @@ function mod_page_ip($cip) { mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($cip)), 'mod/view_ip.html', $args, $args['hostname'] ?? null); } +function mod_announcements() { + global $config; + global $mod; + + if (!hasPermission($config['mod']['announcements'])) + error($config['error']['noaccess']); + + // Add, edit, or delete announcement + if (isset($_POST['announcement'], $_POST['id'])) { + if ($_POST['id'] == '-1') { + Announcements::new_announcement($_POST['announcement']); + } else if ($_POST['announcement'] == '') { + Announcements::delete_announcement($_POST['id']); + } else { + Announcements::edit_announcement($_POST['id'], $_POST['announcement']); + } + } + + $query = query("SELECT ``announcements``.*, `username` FROM ``announcements`` + LEFT JOIN ``mods`` ON ``mods``.`id` = `creator` + ORDER BY `date` DESC") or error(db_error($query)); + $announcements = $query->fetchAll(PDO::FETCH_ASSOC); + + foreach ($announcements as &$announce) { + $announce['date_formated'] = strftime($config['announcements']['date_format'], $announce['date']); + } + + // Display announcement page + mod_page(_('Announcements list'), 'announcements_list.html', array( + 'announcements' => $announcements, + 'mod' => $mod, + 'token' => make_secure_link_token('announcements'), + )); +} + function mod_ban() { global $config; diff --git a/install.php b/install.php index 09aa1671..0c3b99dd 100644 --- a/install.php +++ b/install.php @@ -636,6 +636,14 @@ if (file_exists($config['has_installed'])) { PRIMARY KEY (`cookie`,`extra`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;') or error(db_error()); case '5.1.4': + query('CREATE TABLE IF NOT EXISTS `announcements` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `creator` int(10) NOT NULL, + `date` int(10) NOT NULL, + `text` text NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + ') or error(db_error()); query('CREATE TABLE IF NOT EXISTS ``filehashes`` ( `id` int(11) NOT NULL AUTO_INCREMENT, `board` varchar(58) NOT NULL, @@ -878,6 +886,13 @@ if ($step == 0) { 'required' => true, 'message' => 'vichan does not have permission to create directories (boards) here. You will need to chmod (or operating system equivalent) appropriately.' ), + array( + 'category' => 'File permissions', + 'name' => getcwd() . '/templates/generated', + 'result' => is_dir('templates/generated') && is_writable('templates/generated'), + 'required' => true, + 'message' => 'You must give vichan permission to write to the templates/generated directory.' + ), array( 'category' => 'File permissions', 'name' => getcwd() . '/templates/cache', @@ -958,6 +973,9 @@ if ($step == 0) { $more = $_POST['more']; unset($_POST['more']); + // Generate empty templates that are assumed to exist by other templates + file_write("templates/generated/announcements_short.html", ""); + $instance_config = '<'.'?php diff --git a/install.sql b/install.sql index b2037bee..9126ed17 100644 --- a/install.sql +++ b/install.sql @@ -17,6 +17,19 @@ SET time_zone = "+00:00"; -- -------------------------------------------------------- +-- +-- Table structure for table `announcements` +-- + +CREATE TABLE IF NOT EXISTS `announcements` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `creator` int(10) NOT NULL, + `date` int(10) NOT NULL, + `text` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- -- -- Table structure for table `antispam` -- diff --git a/mod.php b/mod.php index bdc56149..d83c4058 100644 --- a/mod.php +++ b/mod.php @@ -49,6 +49,8 @@ $pages = array( '/noticeboard' => 'secure_POST noticeboard', // view noticeboard '/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard '/noticeboard/delete/(\d+)' => 'secure noticeboard_delete', // delete from noticeboard + + '/announcements' => 'secure_POST announcements', // announcement list '/edit/(\%b)' => 'secure_POST edit_board', // edit board details '/new-board' => 'secure_POST new_board', // create a new board diff --git a/stylesheets/style.css b/stylesheets/style.css index ba95f5d4..d50f2a97 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -532,6 +532,30 @@ div.blotter { text-align: center; } +table.announcements { + text-align: center; + width: 480px; + margin-left: auto; + margin-right: auto; + font-size: 0.8em; +} +table.announcements thead td { + border-bottom: 1px solid rgb(63, 63, 63); + border-bottom: 1px solid rgba(0, 0, 0, .4); + -webkit-background-clip: padding-box; /* for Safari */ + background-clip: padding-box; /* for IE9+, Firefox 4+, Opera, Chrome */ +} +table.announcements tbody { + text-align: left; +} +table.announcements tbody td:first-child { + text-align: right; + vertical-align: top; +} +table.announcements tfoot td { + text-align: right; +} + table.mod.config-editor { font-size: 9pt; width: 100%; diff --git a/templates/announcements.html b/templates/announcements.html new file mode 100644 index 00000000..e62af380 --- /dev/null +++ b/templates/announcements.html @@ -0,0 +1,21 @@ + + + + + + + +{% for announcement in announcements %} + + + +{% endfor %} + + + + + + +
 
{{ announcement.date_formated }}{{ announcement.text }}
+ [Show All] +
\ No newline at end of file diff --git a/templates/announcements_list.html b/templates/announcements_list.html new file mode 100644 index 00000000..546beabb --- /dev/null +++ b/templates/announcements_list.html @@ -0,0 +1,66 @@ + + + + + +{% if mod %} +
+

To remove an announcement, make it empty then press Update.

+

You will need to {% trans 'Rebuild' %} pages after this to display the new announcements.

+
+ + + + + + + + + + + + + {% if token %} + + {% endif %} + + + + + + + + {% for announcement in announcements %} + + + {% if token %} + + {% endif %} + + + + + + + + {% endfor %} + +
DateAnnouncementStaffAction
->
-
{{ announcement.date_formated }}
{{ announcement.username }}
+{% else %} + + + + + + + + + {% for announcement in announcements %} + + + + + {% endfor %} + +
DateAnnouncement
{{ announcement.date_formated }}{{ announcement.text }}
+{% endif %} diff --git a/templates/generated/.gitkeep b/templates/generated/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/templates/generic_page.html b/templates/generic_page.html index 9d4716ef..73a212de 100644 --- a/templates/generic_page.html +++ b/templates/generic_page.html @@ -23,6 +23,8 @@ {% include 'attention_bar.html' %} {% include 'post_form.html' %} + {% if config.announcements.show %}{% include 'generated/announcements_short.html' %}{% endif %} + {% if config.global_message %}
{{ config.global_message }}
{% endif %}
diff --git a/templates/index.html b/templates/index.html index da89b47b..f169b864 100644 --- a/templates/index.html +++ b/templates/index.html @@ -55,6 +55,8 @@ {% endif %} + {% if config.announcements.show %}{% include 'generated/announcements_short.html' %}{% endif %} + {% if config.global_message %}
{{ config.global_message }}
{% endif %}
diff --git a/templates/main.js b/templates/main.js index 3b32f916..c8d92b18 100644 --- a/templates/main.js +++ b/templates/main.js @@ -369,6 +369,12 @@ function init() { if (window.location.hash.indexOf('q') != 1 && window.location.hash.substring(1)) highlightReply(window.location.hash.substring(1)); + + {% endverbatim %} + {% if config.announcements.show %} + init_announcements(); + {% endif %} + {% verbatim %} } var RecaptchaOptions = { @@ -405,3 +411,25 @@ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(sc, s); {% endif %} +{% if config.announcements.show %} +function init_announcements() { + $("#announcements-show-all").prepend('[Hide]'); + var hideAnnouncements = (localStorage.getItem('hideAnnouncements') !== null); + toggleAnnouncementList(hideAnnouncements); + + $("#toggle-announcements").click(function() { + var hide = (localStorage.getItem('hideAnnouncements') !== null); + toggleAnnouncementList(!hide); + if (hide) { + localStorage.removeItem('hideAnnouncements'); + } else { + localStorage.setItem('hideAnnouncements', true); + } + }); +} + +function toggleAnnouncementList(hide) { + $("#announcements-body").attr("hidden", hide); + $("#toggle-announcements").text(hide?"Show":"Hide"); +} +{% endif %} diff --git a/templates/mod/dashboard.html b/templates/mod/dashboard.html index 19c4717d..88199e64 100644 --- a/templates/mod/dashboard.html +++ b/templates/mod/dashboard.html @@ -72,6 +72,9 @@ {% if unread_pms > 0 %}{%endif %}({{ unread_pms }} unread){% if unread_pms > 0 %}{%endif %} + {% if mod|hasPermission(config.mod.announcements) %} +
  • {% trans 'Announcements' %}
  • + {% endif %} diff --git a/templates/thread.html b/templates/thread.html index 32331dcb..21b30436 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -50,6 +50,8 @@ {% include 'post_form.html' %} + {% if config.announcements.show %}{% include 'generated/announcements_short.html' %}{% endif %} + {% if config.global_message %}
    {{ config.global_message }}
    {% endif %}
    diff --git a/tools/rebuild.php b/tools/rebuild.php index 289523a1..eeb40a48 100755 --- a/tools/rebuild.php +++ b/tools/rebuild.php @@ -21,6 +21,7 @@ require dirname(__FILE__) . '/inc/cli.php'; +require_once("inc/announcements.php"); require_once("inc/bans.php"); $start = microtime(true); @@ -99,6 +100,9 @@ foreach($boards as &$board) { } } +// Generate announcement files +Announcements::buildAnnouncements(); + if(!$options['quiet']) printf("Complete! Took %g seconds\n", microtime(true) - $start);