diff --git a/libraries/BBcode.php b/libraries/BBcode.php index 1a686dd..82c1c61 100644 --- a/libraries/BBcode.php +++ b/libraries/BBcode.php @@ -75,11 +75,11 @@ class BBcode ['s', '{param}'], ['spoiler', '{param}'], ['box', '
-
- Click to open
', ], - ['box', '
{option}
-
', ], +
' + . 'Click to open
'], + ['box', '
{option}
' + . '
'], ['quote', '
Quote
{param}
'], ]; diff --git a/libraries/BBcodeDefinitions/Code.php b/libraries/BBcodeDefinitions/Code.php index 73e1718..7a5737e 100644 --- a/libraries/BBcodeDefinitions/Code.php +++ b/libraries/BBcodeDefinitions/Code.php @@ -36,10 +36,12 @@ class Code extends CodeDefinition */ public function asHtml(ElementNode $el) { - return preg_replace( - "#\n*\[code\]\n*(.*?)\n*\[/code\]\n*#s", - '
\\1
', - $el->getAsBBCode() - ); + $content = ""; + + foreach ($el->getChildren() as $child) { + $content .= $child->getAsBBCode(); + } + + return "
{$content}
"; } } diff --git a/libraries/Controllers/UserController.php b/libraries/Controllers/UserController.php index f60e95a..a169aff 100644 --- a/libraries/Controllers/UserController.php +++ b/libraries/Controllers/UserController.php @@ -9,6 +9,8 @@ namespace Sakura\Controllers; use Sakura\Config; use Sakura\DB; +use Sakura\Notification; +use Sakura\Perms\Site; use Sakura\Rank; use Sakura\Router; use Sakura\Template; @@ -104,7 +106,7 @@ class UserController extends Controller global $currentUser; // Check permission - if (!$currentUser->permission(\Sakura\Perms\Site::VIEW_MEMBERLIST)) { + if (!$currentUser->permission(Site::VIEW_MEMBERLIST)) { return Template::render('global/restricted'); } @@ -136,4 +138,42 @@ class UserController extends Controller // Render the template return Template::render('main/memberlist'); } + + public function notifications() + { + // TODO: add friend on/offline messages + global $currentUser; + + // Set json content type + header('Content-Type: application/json; charset=utf-8'); + + return json_encode( + $currentUser->notifications(), + JSON_FORCE_OBJECT | JSON_NUMERIC_CHECK | JSON_BIGINT_AS_STRING + ); + } + + public function markNotification($id = 0) + { + global $currentUser; + + // Check permission + if ($currentUser->permission(Site::DEACTIVATED)) { + return '0'; + } + + // Create the notification object + $alert = new Notification($id); + + // Verify that the currently authed user is the one this alert is for + if ($alert->user !== $currentUser->id) { + return '0'; + } + + // Toggle the read status and save + $alert->toggleRead(); + $alert->save(); + + return '1'; + } } diff --git a/libraries/Notification.php b/libraries/Notification.php new file mode 100644 index 0000000..24f4ffa --- /dev/null +++ b/libraries/Notification.php @@ -0,0 +1,81 @@ + + */ +class Notification +{ + public $id = 0; + public $user = 0; + public $time = 0; + public $read = false; + public $title = "Notification"; + public $text = ""; + public $link = ""; + public $image = ""; + public $timeout = 0; + + public function __construct($id = 0) + { + // Get notification data from the database + $data = DB::table('notifications') + ->where('alert_id', $id) + ->get(); + + // Check if anything was returned and assign data + if ($data) { + $data = $data[0]; + + $this->id = $data->alert_id; + $this->user = $data->user_id; + $this->time = $data->alert_timestamp; + $this->read = intval($data->alert_read) !== 0; + $this->title = $data->alert_title; + $this->text = $data->alert_text; + $this->link = $data->alert_link; + $this->image = $data->alert_img; + $this->timeout = $data->alert_timeout; + } + } + + public function save() + { + // Create submission data, insert and update take the same format + $data = [ + 'user_id' => $this->user, + 'alert_timestamp' => $this->time, + 'alert_read' => $this->read ? 1 : 0, + 'alert_title' => $this->title, + 'alert_text' => $this->text, + 'alert_link' => $this->link, + 'alert_img' => $this->image, + 'alert_timeout' => $this->timeout, + ]; + + // Update if id isn't 0 + if ($this->id) { + DB::table('notifications') + ->where('alert_id', $this->id) + ->update($data); + } else { + $this->id = DB::table('notifications') + ->insertGetId($data); + } + } + + public function toggleRead() + { + // Set read to the negative value of itself + $this->read = !$this->read; + } +} diff --git a/libraries/Router.php b/libraries/Router.php index 7d28d6a..5da4112 100644 --- a/libraries/Router.php +++ b/libraries/Router.php @@ -136,8 +136,6 @@ class Router * * @param array $filters The filters for this group. * @param \Closure $callback The containers - * - * @return string The generated URI. */ public static function group($filters, $callback) { @@ -145,6 +143,17 @@ class Router self::$router->group($filters, $callback); } + /** + * Create filter. + * + * string $name Identifier of the filter + * \Closure $method + */ + public static function filter($name, $method) + { + self::$router->filter($name, $method); + } + /** * Handle requests. * diff --git a/libraries/User.php b/libraries/User.php index 9ece53a..179129f 100644 --- a/libraries/User.php +++ b/libraries/User.php @@ -1194,4 +1194,35 @@ class User // Return success return [1, 'SUCCESS']; } + + /** + * Get all the notifications for this user. + * + * @param int $timeDifference The timeframe of alerts that should be fetched. + * @param bool $excludeRead Whether alerts that are marked as read should be included. + * + * @return array An array with Notification objects. + */ + public function notifications($timeDifference = 0, $excludeRead = true) + { + $alertIds = DB::table('notifications') + ->where('user_id', $this->id); + + if ($timeDifference) { + $alertIds->where('alert_timestamp', '>', time() - $timeDifference); + } + + if ($excludeRead) { + $alertIds->where('alert_read', 0); + } + + $alertIds = array_column($alertIds->get(['alert_id']), 'alert_id'); + $alerts = []; + + foreach ($alertIds as $alertId) { + $alerts[$alertId] = new Notification($alertId); + } + + return $alerts; + } } diff --git a/libraries/Users.php b/libraries/Users.php index b006698..730b4d6 100644 --- a/libraries/Users.php +++ b/libraries/Users.php @@ -350,92 +350,6 @@ class Users } } - /** - * Get a user's notifications. - * - * @param int $uid The user id. - * @param int $timediff The maximum difference in time. - * @param bool $excludeRead Exclude notifications that were already read. - * @param bool $markRead Automatically mark as read. - * - * @return array The notifications. - */ - public static function getNotifications($uid = null, $timediff = 0, $excludeRead = true, $markRead = false) - { - // Prepare conditions - $uid = $uid ? $uid : self::checkLogin()[0]; - $time = $timediff ? time() - $timediff : '%'; - $read = $excludeRead ? '0' : '%'; - - // Get notifications for the database - $alerts = DB::table('notifications') - ->where('user_id', $uid) - ->where('alert_timestamp', '>', $time) - ->where('alert_read', $read) - ->get(); - - // Mark the notifications as read - if ($markRead) { - // Iterate over all entries - foreach ($alerts as $alert) { - // If the notifcation is already read skip - if ($alert->alert_read) { - continue; - } - - // Mark them as read - self::markNotificationRead($notification->alert_id); - } - } - - // Return the notifications - return $notifications; - } - - /** - * Mark a notification as read - * - * @param mixed $id The notification's ID. - * @param mixed $mode Read or unread. - */ - public static function markNotificationRead($id, $mode = true) - { - // Execute an update statement - DB::table('notifications') - ->where('alert_id', $id) - ->update([ - 'alert_read' => ($mode ? 1 : 0), - ]); - } - - /** - * Create a new notification. - * - * @param int $user The user id. - * @param string $title The notification title. - * @param string $text The rest of the text. - * @param int $timeout After how many seconds the notification should disappear. - * @param string $img The image. - * @param string $link The link. - * @param int $sound Whether it should play a noise. - */ - public static function createNotification($user, $title, $text, $timeout = 60000, $img = 'FONT:fa-info-circle', $link = '', $sound = 0) - { - // Insert it into the database - DB::table('notifications') - ->insert([ - 'user_id' => $user, - 'alert_timestamp' => time(), - 'alert_read' => 0, - 'alert_sound' => ($sound ? 1 : 0), - 'alert_title' => $title, - 'alert_text' => $text, - 'alert_link' => $link, - 'alert_img' => $img, - 'alert_timeout' => $timeout, - ]); - } - /** * Get the newest member's ID. * diff --git a/public/content/data/yuuno/js/yuuno.js b/public/content/data/yuuno/js/yuuno.js index 4ea7868..9a65a2c 100644 --- a/public/content/data/yuuno/js/yuuno.js +++ b/public/content/data/yuuno/js/yuuno.js @@ -20,18 +20,18 @@ function notifyUI(content) { alert.className = 'notification-enter'; alert.id = id; // Add the icon - if ((typeof content.img).toLowerCase() === 'undefined' || content.img == null || content.img.length < 2) { + if ((typeof content.image).toLowerCase() === 'undefined' || content.image == null || content.image.length < 2) { aIconCont = document.createElement('div'); aIconCont.className = 'font-icon fa fa-info fa-4x'; } - else if (content.img.substr(0, 5) == 'FONT:') { + else if (content.image.substr(0, 5) == 'FONT:') { aIconCont = document.createElement('div'); - aIconCont.className = 'font-icon fa ' + content.img.replace('FONT:', '') + ' fa-4x'; + aIconCont.className = 'font-icon fa ' + content.image.replace('FONT:', '') + ' fa-4x'; } else { aIconCont = document.createElement('img'); aIconCont.alt = id; - aIconCont.src = content.img; + aIconCont.src = content.image; } aIcon.appendChild(aIconCont); aIcon.className = 'notification-icon'; @@ -58,25 +58,6 @@ function notifyUI(content) { alert.appendChild(aClose); // Append the notification to the document cont.appendChild(alert); - // Play sound if request - if (content.sound) { - // Create the elements - var sound = document.createElement('audio'); - var mp3 = document.createElement('source'); - var ogg = document.createElement('source'); - // Assign attribs - mp3.type = 'audio/mp3'; - ogg.type = 'audio/ogg'; - mp3.src = sakuraVars.content + '/sounds/notify.mp3'; - ogg.src = sakuraVars.content + '/sounds/notify.ogg'; - // Append - sound.appendChild(mp3); - sound.appendChild(ogg); - // Less loud - sound.volume = 0.5; - // And play - sound.play(); - } // If keepalive is 0 keep the notification open forever if (content.timeout > 0) { // Set a timeout and close after an amount @@ -111,7 +92,7 @@ function notifyRequest(session) { } // Create AJAX object var get = new AJAX(); - get.setUrl('/settings.php?request-notifications=true&time=' + Sakura.epoch() + '&session=' + session); + get.setUrl('/notifications'); // Add callbacks get.addCallback(200, function () { // Assign the parsed JSON diff --git a/public/content/data/yuuno/js/yuuno.ts b/public/content/data/yuuno/js/yuuno.ts index 3644065..9db6f31 100644 --- a/public/content/data/yuuno/js/yuuno.ts +++ b/public/content/data/yuuno/js/yuuno.ts @@ -8,9 +8,8 @@ interface Notification { title: string; text: string; link: string; - img: string; + image: string; timeout: number; - sound: boolean; } // Spawns a notification @@ -29,22 +28,22 @@ function notifyUI(content: Notification): void { var aCIcon: HTMLDivElement = document.createElement('div'); var aClear: HTMLDivElement = document.createElement('div'); var aIconCont: any; - + // Add attributes to the main element alert.className = 'notification-enter'; alert.id = id; // Add the icon - if ((typeof content.img).toLowerCase() === 'undefined' || content.img == null || content.img.length < 2) { + if ((typeof content.image).toLowerCase() === 'undefined' || content.image == null || content.image.length < 2) { aIconCont = document.createElement('div'); aIconCont.className = 'font-icon fa fa-info fa-4x'; - } else if (content.img.substr(0, 5) == 'FONT:') { + } else if (content.image.substr(0, 5) == 'FONT:') { aIconCont = document.createElement('div'); - aIconCont.className = 'font-icon fa ' + content.img.replace('FONT:', '') + ' fa-4x'; + aIconCont.className = 'font-icon fa ' + content.image.replace('FONT:', '') + ' fa-4x'; } else { aIconCont = document.createElement('img'); aIconCont.alt = id; - aIconCont.src = content.img; + aIconCont.src = content.image; } aIcon.appendChild(aIconCont); @@ -78,30 +77,6 @@ function notifyUI(content: Notification): void { // Append the notification to the document cont.appendChild(alert); - // Play sound if request - if (content.sound) { - // Create the elements - var sound: HTMLAudioElement = document.createElement('audio'); - var mp3: HTMLSourceElement = document.createElement('source'); - var ogg: HTMLSourceElement = document.createElement('source'); - - // Assign attribs - mp3.type = 'audio/mp3'; - ogg.type = 'audio/ogg'; - mp3.src = sakuraVars.content + '/sounds/notify.mp3'; - ogg.src = sakuraVars.content + '/sounds/notify.ogg'; - - // Append - sound.appendChild(mp3); - sound.appendChild(ogg); - - // Less loud - sound.volume = 0.5; - - // And play - sound.play(); - } - // If keepalive is 0 keep the notification open forever if (content.timeout > 0) { // Set a timeout and close after an amount @@ -128,7 +103,7 @@ function notifyClose(id: string): void { // Opening an alerted link function notifyOpen(id: string): void { var sakuraHref: string = document.getElementById(id).getAttribute('sakurahref'); - + if ((typeof sakuraHref).toLowerCase() !== 'undefined') { window.location.assign(sakuraHref); } @@ -143,7 +118,7 @@ function notifyRequest(session: string): void { // Create AJAX object var get: AJAX = new AJAX(); - get.setUrl('/settings.php?request-notifications=true&time=' + Sakura.epoch() + '&session=' + session); + get.setUrl('/notifications'); // Add callbacks get.addCallback(200, () => { diff --git a/public/content/sounds/notify.mp3 b/public/content/sounds/notify.mp3 deleted file mode 100644 index 6d84cd1..0000000 Binary files a/public/content/sounds/notify.mp3 and /dev/null differ diff --git a/public/content/sounds/notify.ogg b/public/content/sounds/notify.ogg deleted file mode 100644 index 06893f3..0000000 Binary files a/public/content/sounds/notify.ogg and /dev/null differ diff --git a/public/settings.php b/public/settings.php index 103c89a..4963448 100644 --- a/public/settings.php +++ b/public/settings.php @@ -17,7 +17,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification // Include components require_once str_replace(basename(__DIR__), '', dirname(__FILE__)) . 'sakura.php'; -// Notifications +// Notifications (decommissioned) if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notifications']) { // Create the notification container array $notifications = []; @@ -28,20 +28,23 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification && $_REQUEST['time'] > (time() - 1000) && isset($_REQUEST['session']) && $_REQUEST['session'] == session_id()) { // Get the user's notifications from the past forever but exclude read notifications - $userNotifs = Users::getNotifications(null, 0, true, true); + $alerts = $currentUser->notifications(); // Add the proper values to the array - foreach ($userNotifs as $notif) { + foreach ($alerts as $alert) { // Add the notification to the display array - $notifications[$notif['alert_timestamp']] = [ - 'read' => $notif['alert_read'], - 'title' => $notif['alert_title'], - 'text' => $notif['alert_text'], - 'link' => $notif['alert_link'], - 'img' => $notif['alert_img'], - 'timeout' => $notif['alert_timeout'], - 'sound' => $notif['alert_sound'], + $notifications[$alert->id] = [ + 'read' => $alert->read, + 'title' => $alert->title, + 'text' => $alert->text, + 'link' => $alert->link, + 'img' => $alert->image, + 'timeout' => $alert->timeout, + 'sound' => $alert->sound, ]; + + $alert->toggleRead(); + $alert->save(); } } @@ -71,7 +74,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification 'title' => $friend->username . ' is online.', 'text' => '', 'link' => '', - 'img' => '/a/' . $friend->id, + 'img' => Router::route('file.avatar', $friend->id), 'timeout' => 2000, 'sound' => false, ]; @@ -87,7 +90,7 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification 'title' => $friend->username . ' is offline.', 'text' => '', 'link' => '', - 'img' => '/a/' . $friend->id, + 'img' => Router::route('file.avatar', $friend->id), 'timeout' => 2000, 'sound' => false, ]; @@ -377,16 +380,20 @@ if (isset($_REQUEST['request-notifications']) && $_REQUEST['request-notification if (array_key_exists($action[1], $notifStrings)) { // Get the current user's profile data $user = User::construct($currentUser->id); + $friend = User::construct($_REQUEST[(isset($_REQUEST['add']) ? 'add' : 'remove')]); - Users::createNotification( - $_REQUEST[(isset($_REQUEST['add']) ? 'add' : 'remove')], - sprintf($notifStrings[$action[1]][0], $user->username), - $notifStrings[$action[1]][1], - 60000, - Router::route('file.avatar', $user->id), - Router::route('user.profile', $user->id), - '1' - ); + $alert = new Notification; + + $alert->user = $friend->id; + $alert->time = time(); + $alert->sound = true; + $alert->title = sprintf($notifStrings[$action[1]][0], $user->username); + $alert->text = $notifStrings[$action[1]][1]; + $alert->image = Router::route('file.avatar', $user->id); + $alert->timeout = 60000; + $alert->link = Router::route('user.profile', $user->id); + + $alert->save(); } } @@ -1490,11 +1497,6 @@ if (Users::checkLogin()) { $renderData['messages'] = []; break; - // Notification history - case 'notifications.history': - $renderData['alerts'] = array_reverse(Users::getNotifications(null, 0, false, true)); - break; - // Avatar and background sizes case 'appearance.avatar': case 'appearance.background': diff --git a/routes.php b/routes.php index d59ab03..0ee16b2 100644 --- a/routes.php +++ b/routes.php @@ -6,6 +6,32 @@ // Define namespace namespace Sakura; +// Check if logged out +Router::filter('logoutCheck', function () { + global $currentUser; + + if ($currentUser->id !== 0) { + $message = "You must be logged out to do that!"; + + Template::vars(['page' => compact('message')]); + + return Template::render('global/information'); + } +}); + +// Check if logged in +Router::filter('loginCheck', function () { + global $currentUser; + + if ($currentUser->id === 0) { + $message = "You must be logged in to do that!"; + + Template::vars(['page' => compact('message')]); + + return Template::render('global/information'); + } +}); + // Meta pages Router::get('/', 'MetaController@index', 'main.index'); Router::get('/faq', 'MetaController@faq', 'main.faq'); @@ -13,16 +39,20 @@ Router::get('/search', 'MetaController@search', 'main.search'); Router::get('/p/{id}', 'MetaController@infoPage', 'main.infopage'); // Auth -Router::get('/login', 'AuthController@loginGet', 'auth.login'); -Router::post('/login', 'AuthController@loginPost', 'auth.login'); -Router::get('/logout', 'AuthController@logout', 'auth.logout'); -Router::get('/register', 'AuthController@registerGet', 'auth.register'); -Router::post('/register', 'AuthController@registerPost', 'auth.register'); -Router::get('/resetpassword', 'AuthController@resetPasswordGet', 'auth.resetpassword'); -Router::post('/resetpassword', 'AuthController@resetPasswordPost', 'auth.resetpassword'); -Router::get('/reactivate', 'AuthController@reactivateGet', 'auth.reactivate'); -Router::post('/reactivate', 'AuthController@reactivatePost', 'auth.reactivate'); -Router::get('/activate', 'AuthController@activate', 'auth.activate'); +Router::group(['before' => 'logoutCheck'], function () { + Router::get('/login', 'AuthController@loginGet', 'auth.login'); + Router::post('/login', 'AuthController@loginPost', 'auth.login'); + Router::get('/register', 'AuthController@registerGet', 'auth.register'); + Router::post('/register', 'AuthController@registerPost', 'auth.register'); + Router::get('/resetpassword', 'AuthController@resetPasswordGet', 'auth.resetpassword'); + Router::post('/resetpassword', 'AuthController@resetPasswordPost', 'auth.resetpassword'); + Router::get('/reactivate', 'AuthController@reactivateGet', 'auth.reactivate'); + Router::post('/reactivate', 'AuthController@reactivatePost', 'auth.reactivate'); + Router::get('/activate', 'AuthController@activate', 'auth.activate'); +}); +Router::group(['before' => 'loginCheck'], function () { + Router::get('/logout', 'AuthController@logout', 'auth.logout'); +}); // News Router::group(['prefix' => 'news'], function () { @@ -37,9 +67,11 @@ Router::group(['prefix' => 'forum'], function () { Router::group(['prefix' => 'post'], function () { Router::get('/{id:i}', 'ForumController@post', 'forums.post'); Router::get('/{id:i}/raw', 'ForumController@postRaw', 'forums.post.raw'); - Router::get('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete'); - Router::post('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete'); - Router::post('/{id:i}/edit', 'ForumController@editPost', 'forums.post.edit'); + Router::group(['before' => 'loginCheck'], function () { + Router::get('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete'); + Router::post('/{id:i}/delete', 'ForumController@deletePost', 'forums.post.delete'); + Router::post('/{id:i}/edit', 'ForumController@editPost', 'forums.post.edit'); + }); }); // Thread @@ -66,6 +98,8 @@ Router::group(['prefix' => 'members'], function () { // User Router::get('/u/{id}', 'UserController@profile', 'user.profile'); Router::get('/u/{id}/header', 'FileController@header', 'user.header'); +Router::get('/notifications', 'UserController@notifications', 'user.notifications'); +Router::get('/notifications/{id}/mark', 'UserController@markNotification', 'user.notifications.mark'); // Files Router::get('/a/{id}', 'FileController@avatar', 'file.avatar'); diff --git a/sakura.php b/sakura.php index 8dac73a..8ef0d95 100644 --- a/sakura.php +++ b/sakura.php @@ -8,7 +8,7 @@ namespace Sakura; // Define Sakura version -define('SAKURA_VERSION', 20160325); +define('SAKURA_VERSION', 20160326); // Define Sakura Path define('ROOT', __DIR__ . '/'); diff --git a/templates/yuuno/global/master.twig b/templates/yuuno/global/master.twig index f15f22c..a2b3dab 100644 --- a/templates/yuuno/global/master.twig +++ b/templates/yuuno/global/master.twig @@ -212,6 +212,12 @@ timeElems[timeElem].innerText = Sakura.timeElapsed(Math.floor(parsed / 1000)); } } + + notifyRequest('{{ php.sessionid }}'); + + setInterval(function() { + notifyRequest('{{ php.sessionid }}'); + }, 60000); {% if sakura.dev.showChangelog and stats %} diff --git a/templates/yuuno/settings/notifications.history.twig b/templates/yuuno/settings/notifications.history.twig index fdb823e..844de4e 100644 --- a/templates/yuuno/settings/notifications.history.twig +++ b/templates/yuuno/settings/notifications.history.twig @@ -1,4 +1,4 @@ -{% set alerts = alerts|batch(10) %} +{% set alerts = user.notifications(0, false)|batch(10) %} {% set paginationPages = alerts %} {% set paginationUrl %}{{ urls.format('SETTING_MODE', ['notifications', 'history']) }}{% endset %} @@ -14,25 +14,25 @@ {% if alerts %}
{% for alert in alerts[get.page|default(1) - 1] %} - +
- {% if 'FONT:' in alert.alert_img %} -
+ {% if 'FONT:' in alert.image %} +
{% else %} - Notification + Notification {% endif %}
- {{ alert.alert_title }} + {{ alert.title }}
- {{ alert.alert_text }} + {{ alert.title }}
- +