The beginning of TypeScript

This commit is contained in:
flash 2016-07-31 21:36:13 +02:00
parent 5d665bce51
commit 97593040e0
72 changed files with 1450 additions and 1285 deletions

View file

@ -0,0 +1,161 @@
<?php
/**
* Holds the forum pages controllers.
*
* @package Sakura
*/
namespace Sakura\Controllers\Forum;
use Sakura\ActiveUser;
use Sakura\Config;
use Sakura\DB;
use Sakura\Forum\Forum;
use Sakura\Forum\Post;
use Sakura\Forum\Topic;
use Sakura\Perms\Forum as ForumPerms;
use Sakura\User;
/**
* Forum page controllers.
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class ForumController extends Controller
{
public function index()
{
// Get the most active topics
$activeTopicsIds = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->groupBy('topic_id')
->orderByRaw('COUNT(*) DESC')
->limit(10)
->get(['topic_id']);
$activeTopics = [];
// make this not disgusting
while (list($_n, $_t) = each($activeTopicsIds)) {
$topic = new Topic($_t->topic_id);
$forum = new Forum($topic->forum);
// Check if we have permission to view it
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$fetch = DB::table('posts')
->groupBy('topic_id')
->orderByRaw('COUNT(*) DESC')
->skip(11 + $_n)
->take(1)
->get(['topic_id']);
if ($fetch) {
$activeTopicsIds[] = $fetch[0];
}
continue;
}
$activeTopics[$topic->id] = $topic;
}
// Get the latest posts
$latestPostsIds = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->orderBy('post_id', 'desc')
->limit(10)
->get(['post_id']);
$latestPosts = [];
while (list($_n, $_p) = each($latestPostsIds)) {
$post = new Post($_p->post_id);
$forum = new Forum($post->forum);
// Check if we have permission to view it
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$fetch = DB::table('posts')
->orderBy('post_id', 'desc')
->skip(11 + $_n)
->take(1)
->get(['post_id']);
if ($fetch) {
$latestPostsIds[] = $fetch[0];
}
continue;
}
$latestPosts[$post->id] = $post;
}
// Get the most active poster
$activePosterId = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->where('post_time', '>', time() - (24 * 60 * 60))
->groupBy('poster_id')
->orderByRaw('COUNT(*) DESC')
->limit(1)
->get(['poster_id']);
$activePoster = User::construct(
$activePosterId ? $activePosterId[0]->poster_id : 0
);
$forum = new Forum;
return view('forum/index', compact('forum', 'activeTopics', 'latestPosts', 'activePoster'));
}
public function forum($id = 0)
{
$forum = new Forum($id);
$redirect = route('forums.index');
$message = "The forum you tried to access does not exist!";
// Redirect forum id 0 to the main page
if ($forum->id === 0) {
return header("Location: {$redirect}");
}
// Check if the forum exists
if ($forum->id < 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
return view('global/information', compact('message', 'redirect'));
}
// Check if the forum isn't a link
if ($forum->type === 2) {
$message = "The forum you tried to access is a link. You're being redirected.";
$redirect = $forum->link;
return view('global/information', compact('message', 'redirect'));
}
return view('forum/forum', compact('forum'));
}
public function markRead($id = 0)
{
$redirect = route('forums.index');
if (!session_check('s')) {
$message = "Your session expired! Go back and try again.";
return view('global/information', compact('message', 'redirect'));
}
$forum = new Forum($id);
// Check if the forum exists
if ($forum->id < 1
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$message = "The forum you tried to access does not exist.";
return view('global/information', compact('message', 'redirect'));
}
$forum->trackUpdateAll(ActiveUser::$user->id);
$message = 'All topics have been marked as read!';
$redirect = route('forums.forum', $forum->id);
return view('global/information', compact('message', 'redirect'));
}
}

View file

@ -0,0 +1,243 @@
<?php
/**
* Holds the controller for posts.
*
* @package Sakura
*/
namespace Sakura\Controllers\Forum;
use Sakura\ActiveUser;
use Sakura\DB;
use Sakura\Forum\Forum;
use Sakura\Forum\Post;
use Sakura\Forum\Topic;
use Sakura\Perms\Forum as ForumPerms;
/**
* Topic controller.
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class PostController extends Controller
{
public function find($id = 0)
{
$post = new Post($id);
$topic = new Topic($post->topic);
$forum = new Forum($topic->forum);
// Check if the forum exists
if ($post->id === 0
|| $topic->id === 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = route('forums.index');
return view('global/information', compact('message', 'redirect'));
}
$topicLink = route('forums.topic', $topic->id);
// Get all post ids from the database
$postIds = DB::table('posts')
->where('topic_id', $topic->id)
->get(['post_id']);
$postIds = array_column($postIds, 'post_id');
// Find in array
$postAt = ceil(array_search($post->id, $postIds) / 10);
// Only append the page variable if it's more than 1
if ($postAt > 1) {
$topicLink .= "?page={$postAt}";
}
return header("Location: {$topicLink}#p{$post->id}");
}
public function raw($id = 0)
{
$post = new Post($id);
$topic = new Topic($post->topic);
$forum = new Forum($topic->forum);
// Check if the forum exists
if ($post->id === 0
|| $topic->id === 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
return "";
}
return $post->text;
}
public function edit($id = 0)
{
$title = $_POST['title'] ?? null;
$text = $_POST['text'] ?? null;
$post = new Post($id);
$topic = new Topic($post->topic);
$forum = new Forum($topic->forum);
// Check permissions
$noAccess = $post->id === 0
|| $topic->id === 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id);
$noEdit = (
$post->poster->id === ActiveUser::$user->id
? !ActiveUser::$user->permission(ForumPerms::EDIT_OWN, Perms::FORUM)
: !$forum->permission(ForumPerms::EDIT_ANY, ActiveUser::$user->id)
) || (
$topic->status === 1
&& !$forum->permission(ForumPerms::LOCK, ActiveUser::$user->id)
);
// Check if the forum exists
if ($noAccess || $noEdit) {
if ($noDelete) {
$message = "You aren't allowed to edit posts in this topic!";
$redirect = route('forums.post', $post->id);
} else {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = route('forums.index');
}
return view('global/information', compact('message', 'redirect'));
}
$titleLength = strlen($title);
$textLength = strlen($text);
$titleMin = config('forum.min_title_length');
$titleMax = config('forum.max_title_length');
$textMin = config('forum.min_post_length');
$textMax = config('forum.max_post_length');
// Checks
$titleTooShort = $title !== null
&& $post->id === $topic->firstPost()->id
&& $titleLength < $titleMin;
$titleTooLong = $title !== null
&& $post->id === $topic->firstPost()->id
&& $titleLength > $titleMax;
$textTooShort = $textLength < $textMin;
$textTooLong = $textLength > $textMax;
// Check requirments
if ($titleTooShort
|| $titleTooLong
|| $textTooShort
|| $textTooLong) {
$message = "";
if ($titleTooShort) {
$message = "This title is too short!";
} elseif ($titleTooLong) {
$message = "This title is too long!";
} elseif ($textTooShort) {
$message = "Please make your post a little bit longer!";
} elseif ($textTooLong) {
$message = "Your post is too long, you're gonna have to cut a little!";
}
$redirect = route('forums.post', $post->id);
if (!isset($_SESSION['replyText'])) {
$_SESSION['replyText'] = [];
}
$_SESSION['replyText']["t{$forum->id}"] = $text;
return view('global/information', compact('message', 'redirect'));
}
unset($_SESSION['replyText']["t{$forum->id}"]);
if ($post->id !== $topic->firstPost()->id || $title === null) {
$title = "Re: {$topic->title}";
} else {
$topic->title = $title;
$topic->update();
}
// Create the post
$post->subject = $title;
$post->text = $text;
$post->editTime = time();
$post->editReason = '';
$post->editUser = ActiveUser::$user;
$post = $post->update();
$postLink = route('forums.post', $post->id);
return header("Location: {$postLink}");
}
public function delete($id = 0)
{
$action = isset($_POST['yes']) && session_check();
$post = new Post($id);
$topic = new Topic($post->topic);
$forum = new Forum($topic->forum);
// Check permissions
$noAccess = $post->id === 0
|| $topic->id === 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id);
$noDelete = (
$post->poster->id === ActiveUser::$user->id
? !ActiveUser::$user->permission(ForumPerms::DELETE_OWN, Perms::FORUM)
: !$forum->permission(ForumPerms::DELETE_ANY, ActiveUser::$user->id)
) || (
$topic->status === 1
&& !$forum->permission(ForumPerms::LOCK, ActiveUser::$user->id)
);
// Check if the forum exists
if ($noAccess || $noDelete) {
if ($noDelete) {
$message = "You aren't allowed to delete posts in this topic!";
$redirect = Router::route('forums.post', $post->id);
} else {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = Router::route('forums.index');
}
return view('global/information', compact('message', 'redirect'));
}
if ($action !== null) {
if ($action) {
// Set message
$message = "Deleted the post!";
// Check if the topic only has 1 post
if ($topic->replyCount() === 1) {
// Delete the entire topic
$topic->delete();
$redirect = route('forums.forum', $forum->id);
} else {
// Just delete the post
$post->delete();
$redirect = route('forums.topic', $topic->id);
}
return view('global/information', compact('message', 'redirect'));
}
$postLink = route('forums.post', $post->id);
return header("Location: {$postLink}");
}
$message = "Are you sure?";
return view('global/confirm', compact('message'));
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Holds the controller for topic.
* Holds the controller for topics.
*
* @package Sakura
*/
@ -9,9 +9,9 @@ namespace Sakura\Controllers\Forum;
use Sakura\ActiveUser;
use Sakura\Forum\Forum;
use Sakura\Forum\Post;
use Sakura\Forum\Topic;
use Sakura\Perms\Forum as ForumPerms;
use Sakura\Template;
/**
* Topic controller.
@ -23,35 +23,22 @@ class TopicController extends Controller
{
public function view($id = 0)
{
// Attempt to get the topic
$topic = new Topic($id);
// And attempt to get the forum
$forum = new Forum($topic->forum);
// Check if the forum exists
if ($topic->id === 0 || !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
// Set render data
Template::vars([
'message' => "This topic doesn't exist or you don't have access to it!",
'redirect' => route('forums.index'),
]);
if ($topic->id === 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$message = "This topic doesn't exist or you don't have access to it!";
$redirect = route('forums.index');
// Print page contents
return Template::render('global/information');
return view('global/information', compact('message', 'redirect'));
}
// Update the tracking status
$topic->trackUpdate(ActiveUser::$user->id);
// Update views
$topic->viewsUpdate();
// Set parse variables
Template::vars(compact('forum', 'topic'));
// Print page contents
return Template::render('forum/topic');
return view('forum/topic', compact('forum', 'topic'));
}
private function modBase($id)
@ -59,7 +46,9 @@ class TopicController extends Controller
$topic = new Topic($id);
$forum = new Forum($topic->forum);
if ($topic->id !== 0 || $forum->permission(ForumPerms::VIEW, ActiveUser::$user->id) || session_check()) {
if ($topic->id !== 0
|| $forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)
|| session_check()) {
return compact('topic', 'forum');
}
@ -240,9 +229,7 @@ class TopicController extends Controller
$message = "This post doesn't exist or you don't have access to it!";
$redirect = route('forums.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
return view('global/information', compact('message', 'redirect'));
}
// Check if the topic exists
@ -254,9 +241,7 @@ class TopicController extends Controller
$message = "You are not allowed to post in this topic!";
$redirect = route('forums.topic', $topic->id);
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
return view('global/information', compact('message', 'redirect'));
}
// Length
@ -269,7 +254,7 @@ class TopicController extends Controller
// Check requirments
if ($tooShort
|| $tooLong) {
$route = Router::route('forums.topic', $topic->id);
$route = route('forums.topic', $topic->id);
$message = "Your post is " . (
$tooShort
@ -278,15 +263,13 @@ class TopicController extends Controller
);
$redirect = "{$route}#reply";
Template::vars(compact('message', 'redirect'));
if (!isset($_SESSION['replyText'])) {
$_SESSION['replyText'] = [];
}
$_SESSION['replyText']["t{$topic->id}"] = $text;
return Template::render('global/information');
return view('global/information', compact('message', 'redirect'));
}
unset($_SESSION['replyText']["t{$topic->id}"]);
@ -306,4 +289,90 @@ class TopicController extends Controller
// Head to the post
return header("Location: {$postLink}");
}
public function create($id = 0)
{
$title = $_POST['title'] ?? null;
$text = $_POST['text'] ?? null;
// And attempt to get the forum
$forum = new Forum($id);
// Check if the forum exists
if ($forum->id === 0
|| $forum->type !== 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)
|| !$forum->permission(ForumPerms::REPLY, ActiveUser::$user->id)
|| !$forum->permission(ForumPerms::CREATE_THREADS, ActiveUser::$user->id)) {
$message = "This forum doesn't exist or you don't have access to it!";
$redirect = route('forums.index');
return view('global/information', compact('message', 'redirect'));
}
if ($text && $title) {
// Length
$titleLength = strlen($title);
$textLength = strlen($text);
$titleMin = config('forum.min_title_length');
$titleMax = config('forum.max_title_length');
$textMin = config('forum.min_post_length');
$textMax = config('forum.max_post_length');
// Checks
$titleTooShort = $titleLength < $titleMin;
$titleTooLong = $titleLength > $titleMax;
$textTooShort = $textLength < $textMin;
$textTooLong = $textLength > $textMax;
// Check requirments
if ($titleTooShort
|| $titleTooLong
|| $textTooShort
|| $textTooLong) {
$message = "";
if ($titleTooShort) {
$message = "This title is too short, it has to be longer than {$titleMin} characters!";
} elseif ($titleTooLong) {
$message = "This title is too long, keep it under {$titleMax} characters!";
} elseif ($textTooShort) {
$message = "Please make your post a little bit longer, at least {$textMin} characters!";
} elseif ($textTooLong) {
$message = "Your post is too long, you're gonna have to cut a little!"
. " Can't be more than {$textMax} characters.";
}
$redirect = route('forums.new', $forum->id);
if (!isset($_SESSION['replyText'])) {
$_SESSION['replyText'] = [];
}
$_SESSION['replyText']["f{$forum->id}"]["title"] = $title;
$_SESSION['replyText']["f{$forum->id}"]["text"] = $text;
return view('global/information', compact('message', 'redirect'));
}
unset($_SESSION['replyText']["f{$forum->id}"]);
// Create the post
$post = Post::create(
$title,
$text,
ActiveUser::$user,
0,
$forum->id
);
// Go to the post
$postLink = route('forums.post', $post->id);
// Head to the post
return header("Location: {$postLink}");
}
return view('forum/topic', compact('forum'));
}
}

View file

@ -1,630 +0,0 @@
<?php
/**
* Holds the forum pages controllers.
*
* @package Sakura
*/
namespace Sakura\Controllers;
use Sakura\ActiveUser;
use Sakura\Config;
use Sakura\DB;
use Sakura\Forum\Forum;
use Sakura\Forum\Post;
use Sakura\Forum\Topic;
use Sakura\Perms;
use Sakura\Perms\Forum as ForumPerms;
use Sakura\Router;
use Sakura\Template;
use Sakura\User;
/**
* Forum page controllers.
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class ForumController extends Controller
{
/**
* Serves the forum index.
*
* @return string HTML for the forum index.
*/
public function index()
{
// Get the most active topics
$activeTopicsIds = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->groupBy('topic_id')
->orderByRaw('COUNT(*) DESC')
->limit(10)
->get(['topic_id']);
$activeTopics = [];
// make this not disgusting
while (list($_n, $_t) = each($activeTopicsIds)) {
// Create the topic object
$topic = new Topic($_t->topic_id);
// Create a forum object
$forum = new Forum($topic->forum);
// Check if we have permission to view it
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$fetch = DB::table('posts')
->groupBy('topic_id')
->orderByRaw('COUNT(*) DESC')
->skip(11 + $_n)
->take(1)
->get(['topic_id']);
if ($fetch) {
$activeTopicsIds[] = $fetch[0];
}
continue;
}
$activeTopics[$topic->id] = $topic;
}
// Get the latest posts
$latestPostsIds = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->orderBy('post_id', 'desc')
->limit(10)
->get(['post_id']);
$latestPosts = [];
while (list($_n, $_p) = each($latestPostsIds)) {
// Create new post object
$post = new Post($_p->post_id);
// Forum id
$forum = new Forum($post->forum);
// Check if we have permission to view it
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$fetch = DB::table('posts')
->orderBy('post_id', 'desc')
->skip(11 + $_n)
->take(1)
->get(['post_id']);
if ($fetch) {
$latestPostsIds[] = $fetch[0];
}
continue;
}
$latestPosts[$post->id] = $post;
}
// Get the most active poster
$activePosterId = DB::table('posts')
->where('forum_id', '!=', config('forum.trash'))
->where('post_time', '>', time() - (24 * 60 * 60))
->groupBy('poster_id')
->orderByRaw('COUNT(*) DESC')
->limit(1)
->get(['poster_id']);
$activePoster = User::construct(
$activePosterId ? $activePosterId[0]->poster_id : 0
);
// Create the forum object
$forum = new Forum;
Template::vars(compact('forum', 'activeTopics', 'latestPosts', 'activePoster'));
// Return the compiled page
return Template::render('forum/index');
}
/**
* Get a forum page.
*
* @return string
*/
public function forum($id = 0)
{
// Get the forum
$forum = new Forum($id);
// Redirect forum id 0 to the main page
if ($forum->id === 0) {
return header('Location: ' . Router::route('forums.index'));
}
// Check if the forum exists
if ($forum->id < 0) {
// Set render data
Template::vars([
'page' => [
'message' => 'The forum you tried to access does not exist.',
'redirect' => Router::route('forums.index'),
],
]);
// Print page contents
return Template::render('global/information');
}
// Check if the user has access to the forum
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
// Set render data
Template::vars([
'page' => [
'message' => 'You do not have access to this forum.',
'redirect' => Router::route('forums.index'),
],
]);
// Print page contents
return Template::render('global/information');
}
// Check if the forum isn't a link
if ($forum->type === 2) {
// Set render data
Template::vars([
'page' => [
'message' => 'The forum you tried to access is a link. You\'re being redirected.',
'redirect' => $forum->link,
],
]);
// Print page contents
return Template::render('global/information');
}
// Set parse variables
Template::vars([
'forum' => $forum,
]);
// Print page contents
return Template::render('forum/forum');
}
/**
* Mark a forum as read.
*
* @return string
*/
public function markForumRead($id = 0)
{
// Check if the session id was supplied
if (!isset($_GET['s']) || $_GET['s'] != session_id()) {
// Set render data
Template::vars([
'page' => [
'message' => 'Your session expired! Go back and try again.',
'redirect' => Router::route('forums.index'),
],
]);
// Print page contents
return Template::render('global/information');
}
// Get the forum
$forum = new Forum($id);
// Check if the forum exists
if ($forum->id < 1) {
// Set render data
Template::vars([
'page' => [
'message' => 'The forum you tried to access does not exist.',
'redirect' => Router::route('forums.index'),
],
]);
// Print page contents
return Template::render('global/information');
}
// Check if the user has access to the forum
if (!$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
// Set render data
Template::vars([
'page' => [
'message' => 'You do not have access to this forum.',
'redirect' => Router::route('forums.index'),
],
]);
// Print page contents
return Template::render('global/information');
}
// Run the function
$forum->trackUpdateAll(ActiveUser::$user->id);
// Set render data
Template::vars([
'page' => [
'message' => 'All topics have been marked as read.',
'redirect' => Router::route('forums.forum', $forum->id),
],
]);
// Print page contents
return Template::render('global/information');
}
/**
* Redirect to the position of a post in a topic.
*
* @return mixed
*/
public function post($id = 0)
{
// Attempt to get the post
$post = new Post($id);
// And attempt to get the forum
$topic = new Topic($post->topic);
// And attempt to get the forum
$forum = new Forum($topic->forum);
// Check if the forum exists
if ($post->id == 0
|| $topic->id == 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = Router::route('forums.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Generate link
$topicLink = Router::route('forums.topic', $topic->id);
// Get all post ids from the database
$postIds = DB::table('posts')
->where('topic_id', $topic->id)
->get(['post_id']);
$postIds = array_column($postIds, 'post_id');
// Find in array
$postAt = ceil(array_search($post->id, $postIds) / 10);
// Only append the page variable if it's more than 1
if ($postAt > 1) {
$topicLink .= "?page={$postAt}";
}
return header("Location: {$topicLink}#p{$post->id}");
}
/**
* Get the raw text of a post.
*
* @return string
*/
public function postRaw($id = 0)
{
// Attempt to get the post
$post = new Post($id);
// And attempt to get the forum
$topic = new Topic($post->topic);
// And attempt to get the forum
$forum = new Forum($topic->forum);
// Check if the forum exists
if ($post->id == 0
|| $topic->id == 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)) {
return "";
}
return $post->text;
}
/**
* Create a topic.
*
* @return string
*/
public function createTopic($id = 0)
{
$title = isset($_POST['title']) ? $_POST['title'] : null;
$text = isset($_POST['text']) ? $_POST['text'] : null;
// And attempt to get the forum
$forum = new Forum($id);
// Check if the forum exists
if ($forum->id === 0
|| $forum->type !== 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id)
|| !$forum->permission(ForumPerms::REPLY, ActiveUser::$user->id)
|| !$forum->permission(ForumPerms::CREATE_THREADS, ActiveUser::$user->id)) {
$message = "This forum doesn't exist or you don't have access to it!";
$redirect = Router::route('forums.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
if ($text && $title) {
// Length
$titleLength = strlen($title);
$textLength = strlen($text);
$titleMin = config('forum.min_title_length');
$titleMax = config('forum.max_title_length');
$textMin = config('forum.min_post_length');
$textMax = config('forum.max_post_length');
// Checks
$titleTooShort = $titleLength < $titleMin;
$titleTooLong = $titleLength > $titleMax;
$textTooShort = $textLength < $textMin;
$textTooLong = $textLength > $textMax;
// Check requirments
if ($titleTooShort
|| $titleTooLong
|| $textTooShort
|| $textTooLong) {
$message = "";
if ($titleTooShort) {
$message = "This title is too short!";
} elseif ($titleTooLong) {
$message = "This title is too long!";
} elseif ($textTooShort) {
$message = "Please make your post a little bit longer!";
} elseif ($textTooLong) {
$message = "Your post is too long, you're gonna have to cut a little!";
}
$redirect = Router::route('forums.new', $forum->id);
Template::vars(compact('message', 'redirect'));
if (!isset($_SESSION['replyText'])) {
$_SESSION['replyText'] = [];
}
$_SESSION['replyText']["f{$forum->id}"]["title"] = $title;
$_SESSION['replyText']["f{$forum->id}"]["text"] = $text;
return Template::render('global/information');
}
unset($_SESSION['replyText']["f{$forum->id}"]);
// Create the post
$post = Post::create(
$title,
$text,
ActiveUser::$user,
0,
$forum->id
);
// Go to the post
$postLink = Router::route('forums.post', $post->id);
// Head to the post
return header("Location: {$postLink}");
}
Template::vars(compact('forum'));
return Template::render('forum/topic');
}
/**
* Edit a post.
*
* @return string
*/
public function editPost($id = 0)
{
$title = isset($_POST['title']) ? $_POST['title'] : null;
$text = isset($_POST['text']) ? $_POST['text'] : null;
// Attempt to get the post
$post = new Post($id);
// Attempt to get the topic
$topic = new Topic($post->topic);
// And attempt to get the forum
$forum = new Forum($topic->forum);
// Check permissions
$noAccess = $post->id == 0
|| $topic->id == 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id);
$noEdit = (
$post->poster->id === ActiveUser::$user->id
? !ActiveUser::$user->permission(ForumPerms::EDIT_OWN, Perms::FORUM)
: !$forum->permission(ForumPerms::EDIT_ANY, ActiveUser::$user->id)
) || (
$topic->status === 1
&& !$forum->permission(ForumPerms::LOCK, ActiveUser::$user->id)
);
// Check if the forum exists
if ($noAccess || $noEdit) {
if ($noDelete) {
$message = "You aren't allowed to edit posts in this topic!";
$redirect = Router::route('forums.post', $post->id);
} else {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = Router::route('forums.index');
}
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Length
$titleLength = strlen($title);
$textLength = strlen($text);
$titleMin = config('forum.min_title_length');
$titleMax = config('forum.max_title_length');
$textMin = config('forum.min_post_length');
$textMax = config('forum.max_post_length');
// Checks
$titleTooShort = $title !== null
&& $post->id === $topic->firstPost()->id
&& $titleLength < $titleMin;
$titleTooLong = $title !== null
&& $post->id === $topic->firstPost()->id
&& $titleLength > $titleMax;
$textTooShort = $textLength < $textMin;
$textTooLong = $textLength > $textMax;
// Check requirments
if ($titleTooShort
|| $titleTooLong
|| $textTooShort
|| $textTooLong) {
$message = "";
if ($titleTooShort) {
$message = "This title is too short!";
} elseif ($titleTooLong) {
$message = "This title is too long!";
} elseif ($textTooShort) {
$message = "Please make your post a little bit longer!";
} elseif ($textTooLong) {
$message = "Your post is too long, you're gonna have to cut a little!";
}
$redirect = Router::route('forums.post', $post->id);
Template::vars(compact('message', 'redirect'));
if (!isset($_SESSION['replyText'])) {
$_SESSION['replyText'] = [];
}
$_SESSION['replyText']["t{$forum->id}"] = $text;
return Template::render('global/information');
}
unset($_SESSION['replyText']["t{$forum->id}"]);
if ($post->id !== $topic->firstPost()->id || $title === null) {
$title = "Re: {$topic->title}";
} else {
$topic->title = $title;
$topic->update();
}
// Create the post
$post->subject = $title;
$post->text = $text;
$post->editTime = time();
$post->editReason = '';
$post->editUser = ActiveUser::$user;
$post = $post->update();
// Go to the post
$postLink = Router::route('forums.post', $post->id);
// Head to the post
return header("Location: {$postLink}");
}
/**
* Delete a post.
*
* @return string
*/
public function deletePost($id = 0)
{
$action = isset($_POST['yes']) && isset($_POST['sessionid'])
? $_POST['sessionid'] === session_id()
: null;
// Attempt to get the post
$post = new Post($id);
// And attempt to get the forum
$topic = new Topic($post->topic);
// And attempt to get the forum
$forum = new Forum($topic->forum);
// Check permissions
$noAccess = $post->id == 0
|| $topic->id == 0
|| !$forum->permission(ForumPerms::VIEW, ActiveUser::$user->id);
$noDelete = (
$post->poster->id === ActiveUser::$user->id
? !ActiveUser::$user->permission(ForumPerms::DELETE_OWN, Perms::FORUM)
: !$forum->permission(ForumPerms::DELETE_ANY, ActiveUser::$user->id)
) || (
$topic->status === 1
&& !$forum->permission(ForumPerms::LOCK, ActiveUser::$user->id)
);
// Check if the forum exists
if ($noAccess || $noDelete) {
if ($noDelete) {
$message = "You aren't allowed to delete posts in this topic!";
$redirect = Router::route('forums.post', $post->id);
} else {
$message = "This post doesn't exist or you don't have access to it!";
$redirect = Router::route('forums.index');
}
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
if ($action !== null) {
if ($action) {
// Set message
$message = "Deleted the post!";
// Check if the topic only has 1 post
if ($topic->replyCount() === 1) {
// Delete the entire topic
$topic->delete();
$redirect = Router::route('forums.forum', $forum->id);
} else {
// Just delete the post
$post->delete();
$redirect = Router::route('forums.topic', $topic->id);
}
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
$postLink = Router::route('forums.post', $post->id);
return header("Location: {$postLink}");
}
$message = "Are you sure?";
Template::vars(compact('message'));
return Template::render('global/confirm');
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* Holds the base controller for manage.
*
* @package Sakura
*/
namespace Sakura\Controllers\Manage;
use Sakura\Controllers\Controller as BaseController;
use Sakura\Template;
/**
* Base management controller (which other controllers should extend on).
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class Controller extends BaseController
{
public function __construct()
{
$navigation = $this->navigation();
Template::vars(compact('navigation'));
}
public function navigation()
{
$nav = [];
// Overview
$nav["Overview"]["Index"] = route('manage.overview.index');
return $nav;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* Holds the overview controller.
*
* @package Sakura
*/
namespace Sakura\Controllers\Manage;
use Sakura\DB;
/**
* Overview controller.
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class OverviewController extends Controller
{
public function index()
{
return view('manage/overview/index');
}
public function data()
{
$data = new \stdClass;
$data->postsCount = DB::table('posts')
->count();
$data->topicsCount = DB::table('topics')
->count();
$data->usersCount = DB::table('users')
->count();
$data->commentsCount = DB::table('comments')
->count();
$data->uploadsCount = DB::table('uploads')
->count();
return $this->json($data);
}
}

View file

@ -10,6 +10,7 @@ namespace Sakura;
use Twig_Environment;
use Twig_Extension_StringLoader;
use Twig_Loader_Filesystem;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
/**
@ -51,10 +52,18 @@ class Template
*
* @var array
*/
protected static $utility = [
protected static $utilityFunctions = [
'route',
'config',
'session_id',
];
/**
* List of utility filters to add to templating
*
* @var array
*/
protected static $utilityFilters = [
'json_decode',
'byte_symbol',
];
@ -99,8 +108,13 @@ class Template
self::$engine->addExtension(new Twig_Extension_StringLoader());
// Add utility functions
foreach (self::$utility as $utility) {
self::$engine->addFunction(new Twig_SimpleFunction($utility, $utility));
foreach (self::$utilityFunctions as $function) {
self::$engine->addFunction(new Twig_SimpleFunction($function, $function));
}
// Add utility filters
foreach (self::$utilityFilters as $filter) {
self::$engine->addFilter(new Twig_SimpleFilter($filter, $filter));
}
}

View file

@ -7,7 +7,7 @@ elixir(function(mix) {
mix
.less('aitemu/master.less', 'public/css/aitemu.css')
.less('yuuno/master.less', 'public/css/yuuno.css')
.typescript('master/**/*.ts', 'public/js/master.js')
.typescript('app/**/*.ts', 'public/js/app.js')
.typescript('aitemu/**/*.ts', 'public/js/aitemu.js')
.typescript('yuuno/**/*.ts', 'public/js/yuuno.js')
.scripts([

View file

@ -0,0 +1,147 @@
namespace Sakura
{
export class AJAX
{
// XMLHTTPRequest container
private Request: XMLHttpRequest;
private Callbacks: any;
private Headers: any;
private URL: string;
private Send: string = null;
private Asynchronous: boolean = true;
// Prepares the XMLHttpRequest and stuff
constructor(async: boolean = true) {
this.Request = new XMLHttpRequest();
this.Callbacks = new Object();
this.Headers = new Object();
this.Asynchronous = async;
}
// Start
public Start(method: HTTPMethod, avoidCache: boolean = false): void {
// Open the connection
this.Request.open(HTTPMethod[method], this.URL + (avoidCache ? (this.URL.indexOf("?") < 0 ? '?' : '&') + "no-cache=" + Date.now() : ""), this.Asynchronous);
// Set headers
this.PrepareHeaders();
// Watch the ready state
this.Request.onreadystatechange = () => {
// Only invoke when complete
if (this.Request.readyState === 4) {
// Check if a callback if present
if ((typeof this.Callbacks[this.Request.status]).toLowerCase() === 'function') {
this.Callbacks[this.Request.status](this);
} else { // Else check if there's a generic fallback present
if ((typeof this.Callbacks['0']).toLowerCase() === 'function') {
// Call that
this.Callbacks['0'](this);
}
}
}
}
this.Request.send(this.Send);
}
// Stop
public Stop(): void {
this.Request = null;
}
// Set content type required for forms
public Form(): void {
this.ContentType("application/x-www-form-urlencoded");
}
// Add post data
public SetSend(data: any): void {
// Storage array
var store: Array<string> = new Array<string>();
// Iterate over the object and them in the array with an equals sign inbetween
for (var item in data) {
store.push(encodeURIComponent(item) + "=" + encodeURIComponent(data[item]));
}
// Assign to send
this.Send = store.join('&');
}
// Set raw post
public SetRawSend(data: string): void {
this.Send = data;
}
// Get response
public Response(): string {
return this.Request.responseText;
}
// Automatically JSON parse the response
public JSON(): Object {
return JSON.parse(this.Response());
}
// Get all headers
public ResponseHeaders(): string {
return this.Request.getAllResponseHeaders();
}
// Get a header
public ResponseHeader(name: string): string {
return this.Request.getResponseHeader(name);
}
// Set charset
public ContentType(type: string, charset: string = null): void {
this.AddHeader('Content-Type', type + ';charset=' + (charset ? charset : 'utf-8'));
}
// Add a header
public AddHeader(name: string, value: string): void {
// Attempt to remove a previous instance
this.RemoveHeader(name);
// Add the new header
this.Headers[name] = value;
}
// Remove a header
public RemoveHeader(name: string): void {
if ((typeof this.Headers[name]).toLowerCase() !== 'undefined') {
delete this.Headers[name];
}
}
// Prepare Request headers
public PrepareHeaders(): void {
for (var header in this.Headers) {
this.Request.setRequestHeader(header, this.Headers[header]);
}
}
// Adds a callback
public AddCallback(status: number, callback: Function): void {
// Attempt to remove previous instances
this.RemoveCallback(status);
// Add the new callback
this.Callbacks[status] = callback;
}
// Delete a callback
public RemoveCallback(status: number): void {
// Delete the callback if present
if ((typeof this.Callbacks[status]).toLowerCase() === 'function') {
delete this.Callbacks[status];
}
}
// Sets the URL
public SetUrl(url: string): void {
this.URL = url;
}
}
}

View file

@ -0,0 +1,87 @@
namespace Sakura
{
export class Changelog
{
private static Client: AJAX;
private static Element: DOM;
private static Fetch: number = 0;
private static Colours: string[] = [
'inherit', // Unknown
'#2A2', // Add
'#2AA', // Update
'#2AA', // Fix
'#A22', // Remove
'#62C', // Export
'#C44', // Revert
];
public static Build(target: DOM)
{
this.Client = new AJAX;
this.Element = DOM.Create('table', 'changelog panelTable');
this.Element.Element.style.borderSpacing = '0 1px';
var title: DOM = DOM.Create('div', 'head'),
link: DOM = DOM.Create('a', 'underline');
title.Element.style.marginBottom = '1px';
link.Text('Changelog');
(<HTMLLinkElement>link.Element).href = Config.ChangelogUrl + '#r' + Config.Revision;
(<HTMLLinkElement>link.Element).target = '_blank';
title.Append(link);
target.Append(title);
target.Append(this.Element);
this.Client.SetUrl(Config.ChangelogUrl + Config.ChangelogApi);
this.Client.AddCallback(200, (client: AJAX) => {
Changelog.Add(<IChangelogDate>client.JSON());
if (Changelog.Fetch < 2) {
Changelog.Fetch++;
Changelog.Client.SetUrl(Config.ChangelogUrl + Config.ChangelogApi + Changelog.Fetch);
Changelog.Client.Start(HTTPMethod.GET);
}
});
this.Client.SetUrl(Config.ChangelogUrl + Config.ChangelogApi + Changelog.Fetch);
this.Client.Start(HTTPMethod.GET);
}
private static Add(changelog: IChangelogDate)
{
var header: DOM = DOM.Create('tr', 'changelog__row changelog__row--header'),
headerInner: DOM = DOM.Create('th', 'changelog__header');
headerInner.Text(changelog.date);
headerInner.Element.style.fontSize = '1.2em';
(<HTMLTableHeaderCellElement>headerInner.Element).colSpan = 2;
header.Append(headerInner);
this.Element.Append(header);
for (var _i in changelog.changes)
{
var change: IChangelogChange = changelog.changes[_i],
row: DOM = DOM.Create('tr', 'changelog__row'),
action: DOM = DOM.Create('td', 'changelog__column'),
message: DOM = DOM.Create('td', 'changelog__column');
action.Text(change.action.name);
action.Element.style.backgroundColor = this.Colours[change.action.id];
action.Element.style.borderBottom = '1px solid ' + this.Colours[change.action.id];
message.Text(change.message);
message.Element.style.borderBottom = '1px solid ' + this.Colours[change.action.id];
row.Append(action);
row.Append(message);
this.Element.Append(row);
}
}
}
}

View file

@ -0,0 +1,6 @@
namespace Sakura
{
export class Comments
{
}
}

View file

@ -0,0 +1,21 @@
namespace Sakura
{
export class Config
{
public static Revision: number = 0;
public static SessionId: string = "";
public static UserNameMinLength: number = 3;
public static UserNameMaxLength: number = 16;
public static PasswordMinEntropy: number = 48;
public static LoggedIn: boolean = false;
public static ChangelogUrl: string = "https://sakura.flash.moe/";
public static ChangelogApi: string = "api.php/";
public static Set(config: Object): void
{
for (var key in config) {
this[key] = config[key];
}
}
}
}

View file

@ -0,0 +1,33 @@
namespace Sakura
{
export class Cookies
{
public static Get(name: string): string {
// Do a regex on document.cookie
var get: RegExpExecArray = new RegExp('(^|; )' + encodeURIComponent(name) + '=([^;]*)').exec(document.cookie);
// Return the value
return get ? get[2] : '';
}
public static Set(name: string, value: string): void {
// Check if the cookie already exists
if (this.Get(name).length > 1) {
this.Delete(name);
}
// Lifetime
var life: Date = new Date();
// Cookies live for a year
life.setFullYear(life.getFullYear() + 1);
// Assign the cookie
document.cookie = name + '=' + value + ';expires=' + life.toUTCString();
}
public static Delete(name: string): void {
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
}
}

View file

@ -0,0 +1,74 @@
namespace Sakura
{
export class DOM
{
public Element: HTMLElement;
constructor(object: any, mode: DOMSelector) {
switch (mode) {
case DOMSelector.ID:
this.Element = document.getElementById(object);
break;
case DOMSelector.CLASS:
this.Element = <HTMLElement>document.getElementsByClassName(object)[0];
break;
case DOMSelector.ELEMENT:
this.Element = object;
break;
case DOMSelector.QUERY:
this.Element = <HTMLElement>document.querySelector(object);
break;
}
}
public static Create(element: string, className: string = null, id: string = null): DOM {
var elem: HTMLElement = document.createElement(element),
cont: DOM = new DOM(elem, DOMSelector.ELEMENT);
if (className !== null) {
cont.SetClass(className);
}
if (id !== null) {
cont.SetId(id);
}
return cont;
}
public Text(text: string): void {
this.Element.appendChild(document.createTextNode(text));
}
public Append(element: DOM): void {
this.Element.appendChild(element.Element);
}
public Prepend(element: DOM, before: HTMLElement | Node = null): void {
if (before === null) {
before = this.Element.firstChild;
}
if (this.Element.children.length) {
this.Element.insertBefore(element.Element, before);
} else {
this.Append(element);
}
}
public SetId(id: string): void {
this.Element.id = id;
}
public SetClass(name: string): void {
this.Element.className = name;
}
public Remove(): void {
this.Element.parentNode.removeChild(this.Element);
}
}
}

View file

@ -0,0 +1,10 @@
namespace Sakura
{
export enum DOMSelector
{
ID,
CLASS,
ELEMENT,
QUERY
}
}

View file

@ -0,0 +1,47 @@
declare var ajaxBusyView: any;
namespace Sakura
{
export class Friend
{
private static Client: AJAX;
public static Init(): void
{
this.Client = new AJAX;
this.Client.Form();
}
public static Add(id: number): void
{
this.Mode("/friends/1/add".replace('1', id.toString()));
}
public static Remove(id: number): void
{
this.Mode("/friends/1/remove".replace('1', id.toString()));
}
private static Mode(url: string): void
{
this.Client.SetUrl(url);
this.Client.SetSend({ "session": Config.SessionId });
this.Client.AddCallback(200, (client: AJAX) => {
var response: IFriendResponse = <IFriendResponse>client.JSON(),
error: string = response.error || null;
if (error !== null) {
ajaxBusyView(true, error, 'fail');
setTimeout(() => {
ajaxBusyView(false);
}, 1500);
} else {
ajaxBusyView(true, response.message, 'ok');
location.reload();
}
});
this.Client.Start(HTTPMethod.POST);
}
}
}

View file

@ -0,0 +1,11 @@
namespace Sakura
{
export enum HTTPMethod
{
GET,
HEAD,
POST,
PUT,
DELETE
}
}

View file

@ -0,0 +1,8 @@
namespace Sakura
{
export interface IChangelogAction
{
id: number;
name: string;
}
}

View file

@ -0,0 +1,12 @@
namespace Sakura
{
export interface IChangelogChange
{
id: number;
action?: IChangelogAction;
contributor?: IChangelogContributor;
major: boolean;
url: string;
message: string;
}
}

View file

@ -0,0 +1,9 @@
namespace Sakura
{
export interface IChangelogContributor
{
id: number;
name: string;
url: string;
}
}

View file

@ -0,0 +1,9 @@
namespace Sakura
{
export interface IChangelogDate
{
date: string;
release?: IChangelogRelease;
changes: IChangelogChange[];
}
}

View file

@ -0,0 +1,9 @@
namespace Sakura
{
export interface IChangelogRelease
{
id: number;
colour: string;
name: string;
}
}

View file

@ -0,0 +1,9 @@
namespace Sakura
{
export interface IFriendResponse
{
message: string;
level: number;
error: string;
}
}

View file

@ -0,0 +1,76 @@
declare function escape(a);
namespace Sakura
{
export class Legacy
{
// Alternative for Math.log2() since it's still experimental
public static log2(num: number): number {
return Math.log(num) / Math.log(2);
}
// Get the number of unique characters in a string
public static unique(string: string): number {
// Store the already found character
var used: string[] = [];
// The amount of characters we've already found
var count: number = 0;
// Count the amount of unique characters
for (var i = 0; i < string.length; i++) {
// Check if we already counted this character
if (used.indexOf(string[i]) == -1) {
// Push the character into the used array
used.push(string[i]);
// Up the count
count++;
}
}
// Return the count
return count;
}
// Calculate password entropy
public static entropy(string: string): number {
// Decode utf-8 encoded characters
string = this.utf8_decode(string);
// Count the unique characters in the string
var unique: number = this.unique(string);
// Do the entropy calculation
return unique * this.log2(256);
}
// Validate string lengths
public static stringLength(string: string, minimum: number, maximum: number): boolean {
// Get length of string
var length = string.length;
// Check if it meets the minimum/maximum
if (length < minimum || length > maximum) {
return false;
}
// If it passes both return true
return true;
}
// Validate email address formats
public static validateEmail(email: string): boolean {
// RFC compliant e-mail address regex
var re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,48})+$/;
// Test it on the email var which'll return a boolean
return re.test(email);
}
// Decode a utf-8 string
public static utf8_decode(string): string {
return decodeURIComponent(escape(string));
}
}
}

View file

@ -0,0 +1,15 @@
namespace Sakura
{
export class Main
{
public static Startup(): void {
console.log(this.Supported());
TimeAgo.Init();
Friend.Init();
}
public static Supported(): boolean {
return true;
}
}
}

View file

@ -0,0 +1,56 @@
namespace Sakura
{
export class TimeAgo
{
private static WatchClass: string = "time-ago";
public static Init(): void
{
var watchElements: NodeListOf<Element> = document.getElementsByClassName(this.WatchClass);
for (var _i in watchElements) {
if ((typeof watchElements[_i]).toLowerCase() !== 'object') {
continue;
}
var elem: HTMLElement = <HTMLElement>watchElements[_i],
date: Date = new Date(elem.getAttribute('dateTime') || elem.innerText);
elem.title = elem.innerText;
elem.innerText = this.Parse(date);
}
}
public static Parse(date: Date, append: string = ' ago', none: string = 'Just now'): string {
var time: number = (Date.now() - date.getTime()) / 1000;
if (time < 1) {
return none;
}
var times: Object = {
31536000: ['year', 'a'],
2592000: ['month', 'a'],
604800: ['week', 'a'],
86400: ['day', 'a'],
3600: ['hour', 'an'],
60: ['minute', 'a'],
1: ['second', 'a']
};
var timeKeys: string[] = Object.keys(times).reverse();
for (var i in timeKeys) {
var calc: number = time / parseInt(timeKeys[i]);
if (calc >= 1) {
var display: number = Math.floor(calc);
return (display === 1 ? times[timeKeys[i]][1] : display) + " " + times[timeKeys[i]][0] + (display === 1 ? '' : 's') + append;
}
}
return none;
}
}
}

View file

@ -1,322 +0,0 @@
/*
* Shared client side code
*/
// Meta functions
class Sakura {
public static cookiePrefix: string = ""; // Cookie prefix, gets prepended to cookie names
public static cookiePath: string = "/"; // Cookie path, can in most cases be left untouched
// Get or set a cookie value
public static cookie(name: string, value: string = null): string {
// If value is null only get the cookie's value
if (value) {
// Delete the old instance
document.cookie = this.cookiePrefix + name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT; path=' + this.cookiePath;
// Assign the cookie
document.cookie = this.cookiePrefix + name + '=' + value + '; path=' + this.cookiePath;
// Pass the value through
return value;
} else {
// Perform a regex on document.cookie
var get = new RegExp('(^|; )' + encodeURIComponent(this.cookiePrefix + name) + '=([^;]*)').exec(document.cookie);
// If anything was returned return it (professional phrasing)
return get ? get[2] : '';
}
}
// Unix timestamp
public static epoch(): number {
return Math.floor(Date.now() / 1000);
}
// Toggle a class
public static toggleClass(element: HTMLElement, name: string): void {
// Check if the class already exists and if not add it
if (element.className.indexOf(name) < 0) {
element.className += ' ' + name;
} else { // If so remove it and kill additional spaces
element.className = element.className.replace(name, '').trim();
}
}
// Remove every element with a specific class name
public static removeByClass(name: string): void {
// Get the elements
var objs = document.getElementsByClassName(name);
// Use a while loop to remove each element
while (objs.length > 0) {
objs[0].parentNode.removeChild(objs[0]);
}
}
// Remove a single element with a specific id
public static removeById(id: string): void {
// Get the element
var obj = document.getElementById(id);
// If the element exists use the parent node to remove it
if (typeof (obj) != "undefined" && obj !== null) {
obj.parentNode.removeChild(obj);
}
}
// Alternative for Math.log2() since it's still experimental
public static log2(num: number): number {
return Math.log(num) / Math.log(2);
}
// Get the number of unique characters in a string
public static unique(string: string): number {
// Store the already found character
var used: string[] = [];
// The amount of characters we've already found
var count: number = 0;
// Count the amount of unique characters
for (var i = 0; i < string.length; i++) {
// Check if we already counted this character
if (used.indexOf(string[i]) == -1) {
// Push the character into the used array
used.push(string[i]);
// Up the count
count++;
}
}
// Return the count
return count;
}
// Calculate password entropy
public static entropy(string: string): number {
// Decode utf-8 encoded characters
string = utf8.decode(string);
// Count the unique characters in the string
var unique: number = this.unique(string);
// Do the entropy calculation
return unique * this.log2(256);
}
// Validate string lengths
public static stringLength(string: string, minimum: number, maximum: number): boolean {
// Get length of string
var length = string.length;
// Check if it meets the minimum/maximum
if (length < minimum || length > maximum) {
return false;
}
// If it passes both return true
return true;
}
// Validate email address formats
public static validateEmail(email: string): boolean {
// RFC compliant e-mail address regex
var re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,48})+$/;
// Test it on the email var which'll return a boolean
return re.test(email);
}
// Calculate the time that has elapsed since a certain data (doesn't take leap years in account).
public static timeElapsed(timestamp: number, append: string = ' ago', none: string = 'Just now'): string {
// Subtract the entered timestamp from the current timestamp
var time: number = this.epoch() - timestamp;
// If the new timestamp is below 1 return a standard string
if (time < 1) {
return none;
}
// Times array
var times: Object = {
31536000: ['year', 'a'],
2592000: ['month', 'a'],
604800: ['week', 'a'],
86400: ['day', 'a'],
3600: ['hour', 'an'],
60: ['minute', 'a'],
1: ['second', 'a']
};
//
var timeKeys: string[] = Object.keys(times).reverse();
// Iterate over the times
for (var i in timeKeys) {
// Do a devision to check if the given timestamp fits in the current "type"
var calc: number = time / parseInt(timeKeys[i]);
// Check if we have a match
if (calc >= 1) {
// Round the number
var display: number = Math.floor(calc);
// Return the formatted string
return (display === 1 ? times[timeKeys[i]][1] : display) + " " + times[timeKeys[i]][0] + (display === 1 ? '' : 's') + append;
}
}
// If everything fails just return none
return none;
}
}
declare function unescape(a);
declare function escape(a);
// UTF-8 functions
class utf8 {
// Encode a utf-8 string
public static encode(string): string {
return unescape(encodeURIComponent(string));
}
// Decode a utf-8 string
public static decode(string): string {
return decodeURIComponent(escape(string));
}
}
// HTTP methods
enum HTTPMethods {
GET,
HEAD,
POST,
PUT,
DELETE
}
// AJAX functions
class AJAX {
// XMLHTTPRequest container
private request: XMLHttpRequest;
private callbacks: Object;
private headers: Object;
private url: string;
private send: string = null;
// Prepares the XMLHttpRequest and stuff
constructor() {
this.request = new XMLHttpRequest();
this.callbacks = new Object();
this.headers = new Object();
}
// Start
public start(method: HTTPMethods): void {
// Open the connection
this.request.open(HTTPMethods[method], this.url, true);
// Set headers
this.prepareHeaders();
// Watch the ready state
this.request.onreadystatechange = () => {
// Only invoke when complete
if (this.request.readyState === 4) {
// Check if a callback if present
if ((typeof this.callbacks[this.request.status]).toLowerCase() === 'function') {
this.callbacks[this.request.status]();
} else { // Else check if there's a generic fallback present
if ((typeof this.callbacks['0']).toLowerCase() === 'function') {
// Call that
this.callbacks['0']();
}
}
}
}
this.request.send(this.send);
}
// Stop
public stop(): void {
this.request = null;
}
// Add post data
public setSend(data: Object): void {
// Storage array
var store: Array<string> = new Array<string>();
// Iterate over the object and them in the array with an equals sign inbetween
for (var item in data) {
store.push(encodeURIComponent(item) + "=" + encodeURIComponent(data[item]));
}
// Assign to send
this.send = store.join('&');
}
// Set raw post
public setRawSend(data: string) {
this.send = data;
}
// Get response
public response(): string {
return this.request.responseText;
}
// Set charset
public contentType(type: string, charset: string = null): void {
this.addHeader('Content-Type', type + ';charset=' + (charset ? charset : 'utf-8'));
}
// Add a header
public addHeader(name: string, value: string): void {
// Attempt to remove a previous instance
this.removeHeader(name);
// Add the new header
this.headers[name] = value;
}
// Remove a header
public removeHeader(name: string): void {
if ((typeof this.headers[name]).toLowerCase() !== 'undefined') {
delete this.headers[name];
}
}
// Prepare request headers
public prepareHeaders(): void {
for (var header in this.headers) {
this.request.setRequestHeader(header, this.headers[header]);
}
}
// Adds a callback
public addCallback(status: number, callback: Function): void {
// Attempt to remove previous instances
this.removeCallback(status);
// Add the new callback
this.callbacks[status] = callback;
}
// Delete a callback
public removeCallback(status: number): void {
// Delete the callback if present
if ((typeof this.callbacks[status]).toLowerCase() === 'function') {
delete this.callbacks[status];
}
}
// Sets the URL
public setUrl(url: string): void {
this.url = url;
}
}

View file

@ -0,0 +1,10 @@
namespace Yuuno
{
export class Main
{
public static Startup()
{
Sakura.Main.Startup();
}
}
}

View file

@ -101,7 +101,7 @@ function notifyClose(id: string): void {
// Remove after 410 ms
setTimeout(() => {
Sakura.removeById(id);
(new Sakura.DOM(id, Sakura.DOMSelector.ID)).Remove();
}, 410);
}
@ -122,13 +122,13 @@ function notifyRequest(session: string): void {
}
// Create AJAX object
var get: any = new AJAX();
get.setUrl('/notifications');
var get = new Sakura.AJAX();
get.SetUrl('/notifications');
// Add callbacks
get.addCallback(200, () => {
get.AddCallback(200, () => {
// Assign the parsed JSON
var data: Notification = JSON.parse(get.response());
var data: Notification = JSON.parse(get.Response());
// Check if nothing went wrong
if ((typeof data).toLowerCase() === 'undefined') {
@ -142,7 +142,7 @@ function notifyRequest(session: string): void {
}
});
get.start(HTTPMethods.GET);
get.Start(Sakura.HTTPMethod.GET);
}
// Show the full page busy window
@ -216,7 +216,7 @@ function ajaxBusyView(show: boolean, message: string = null, type: string = null
if (parseInt(cont.style.opacity) > 0) {
cont.style.opacity = (parseInt(cont.style.opacity) - 0.1).toString();
} else {
Sakura.removeById('ajaxBusy');
(new Sakura.DOM('ajaxBusy', Sakura.DOMSelector.ID)).Remove();
clearInterval(out);
}
}, 10);
@ -227,29 +227,29 @@ function ajaxBusyView(show: boolean, message: string = null, type: string = null
// Making a post request using AJAX
function ajaxPost(url: string, data: Object, callback: Function) {
// Create AJAX
var request: any = new AJAX();
var request = new Sakura.AJAX;
// Set url
request.setUrl(url);
request.SetUrl(url);
// Add callbacks
request.addCallback(200, function() {
callback.call(request.response())
request.AddCallback(200, function() {
callback.call(request.Response())
});
request.addCallback(0, function() {
request.AddCallback(0, function() {
ajaxBusyView(false);
throw "POST Request failed";
});
// Add header
request.addHeader('Content-Type', 'application/x-www-form-urlencoded');
request.AddHeader('Content-Type', 'application/x-www-form-urlencoded');
// Set the post data
request.setSend(data);
request.SetSend(data);
// Make the request
request.start(HTTPMethods.POST);
request.Start(Sakura.HTTPMethod.POST);
// Return the AJAX object
return request;
@ -399,7 +399,7 @@ function submitPostHandler(data: string, busyView: boolean, resetCaptcha: boolea
// Check if a password is within the minimum entropy value
function checkPwdEntropy(pwd: string): boolean {
return (Sakura.entropy(pwd) >= sakuraVars.minPwdEntropy);
return (Sakura.Legacy.entropy(pwd) >= sakuraVars.minPwdEntropy);
}
// Check registration variables
@ -420,12 +420,12 @@ function registerVarCheck(id: string, mode: string, option: any = null): void {
break;
case 'email':
check = Sakura.validateEmail(input.value);
check = Sakura.Legacy.validateEmail(input.value);
break;
case 'username':
default:
check = Sakura.stringLength(input.value, sakuraVars.minUserLen, sakuraVars.maxUserLen);
check = Sakura.Legacy.stringLength(input.value, sakuraVars.minUserLen, sakuraVars.maxUserLen);
break;
}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Login' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Reactivate account' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Register' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Reset Password' %}

View file

@ -26,22 +26,22 @@
{% block js %}
<script type="text/javascript">
var commentClient = new AJAX();
var commentClient = new Sakura.AJAX();
commentClient.contentType("application/x-www-form-urlencoded");
commentClient.ContentType("application/x-www-form-urlencoded");
function commentPost(element, url) {
var text = element.querySelector('[name="text"]'),
session = element.querySelector('[name="session"]');
commentClient.setSend({"text":text.value,"session":session.value});
commentClient.SetSend({"text":text.value,"session":session.value});
text.value = '';
commentClient.setUrl(url);
commentClient.SetUrl(url);
commentClient.addCallback(200, function () {
var resp = JSON.parse(commentClient.response());
commentClient.AddCallback(200, function () {
var resp = JSON.parse(commentClient.Response());
if (resp.error) {
ajaxBusyView(true, resp.error, 'fail');
@ -53,10 +53,10 @@
commentAdd(resp);
}
commentClient.setRawSend("");
commentClient.SetRawSend("");
});
commentClient.start(HTTPMethods.POST);
commentClient.Start(Sakura.HTTPMethod.POST);
}
function commentAdd(obj) {
@ -84,7 +84,7 @@
var controlsInner = document.createElement('ul');
if (Sakura.cookie('id') == obj.user) {
if (Sakura.Cookies.Get(sakuraVars.cookie.prefix + 'id') == obj.user) {
var controlsTrashContainer = document.createElement('li');
var controlsTrash = document.createElement('a');
controlsTrash.href = 'javascript:void(0);';
@ -197,7 +197,7 @@
// Remove it if it already exists
if (replyBox) {
Sakura.removeById('comment-reply-container-' + id);
(new Sakura.DOM('comment-reply-container-' + id, Sakura.DOMSelector.ID)).Remove();
return;
}
@ -250,10 +250,10 @@
function commentDelete(id) {
var url = "{{ route('comments.comment.delete', 1) }}".replace(1, id);
commentClient.setUrl(url);
commentClient.SetUrl(url);
commentClient.addCallback(200, function () {
var resp = JSON.parse(commentClient.response());
commentClient.AddCallback(200, function () {
var resp = JSON.parse(commentClient.Response());
if (resp.error) {
ajaxBusyView(true, resp.error, 'fail');
@ -262,11 +262,11 @@
ajaxBusyView(false);
}, 1500);
} else {
Sakura.removeById("comment-" + id);
(new Sakura.DOM("comment-" + id, Sakura.DOMSelector.ID)).Remove();
}
});
commentClient.start(HTTPMethods.POST);
commentClient.Start(Sakura.HTTPMethod.POST);
}
function commentVote(id, vote) {
@ -274,12 +274,12 @@
upvotes = document.getElementById("comment-" + id + "-likes"),
downvotes = document.getElementById("comment-" + id + "-dislikes");
commentClient.setSend({"vote":vote});
commentClient.SetSend({"vote":vote});
commentClient.setUrl(url);
commentClient.SetUrl(url);
commentClient.addCallback(200, function () {
var resp = JSON.parse(commentClient.response());
commentClient.AddCallback(200, function () {
var resp = JSON.parse(commentClient.Response());
if (resp.error) {
ajaxBusyView(true, resp.error, 'fail');
@ -292,10 +292,10 @@
downvotes.innerText = resp.downvotes;
}
commentClient.setRawSend("");
commentClient.SetRawSend("");
});
commentClient.start(HTTPMethods.POST);
commentClient.Start(Sakura.HTTPMethod.POST);
}
</script>
{% endblock %}

View file

@ -20,7 +20,7 @@
All active users in the past 2 minutes
<table class="panelTable">
{% for amount,onlineUser in stats.onlineUsers %}
<tr><td style="text-align: left;"><a href="{{ route('user.profile', onlineUser.id) }}" style="font-weight: bold; color: {{ onlineUser.colour }};" class="default">{{ onlineUser.username }}</a></td><td style="text-align: right;"><time datetime="{{ onlineUser.lastOnline|date('r') }}">{{ onlineUser.lastOnline|date('D Y-m-d H:i:s T') }}</time></td></tr>
<tr><td style="text-align: left;"><a href="{{ route('user.profile', onlineUser.id) }}" style="font-weight: bold; color: {{ onlineUser.colour }};" class="default">{{ onlineUser.username }}</a></td><td style="text-align: right;"><time class="time-ago" datetime="{{ onlineUser.lastOnline|date('r') }}">{{ onlineUser.lastOnline|date('D Y-m-d H:i:s T') }}</time></td></tr>
{% endfor %}
</table>
{% else %}

View file

@ -12,6 +12,6 @@
</div>
<div class="clear"></div>
<div class="news-post-time">
Posted <time datetime="{{ post.time|date('r') }}">{{ post.time|date('D Y-m-d H:i:s T') }}</time>
Posted <time class="time-ago" datetime="{{ post.time|date('r') }}">{{ post.time|date('D Y-m-d H:i:s T') }}</time>
{% if newsHideCommentCount is not defined %}<a class="default" href="{{ route('news.post', post.id) }}#comments">{{ post.commentCount }} comment{% if post.commentCount != 1 %}s{% endif %}</a>{% endif %}
</div>

View file

@ -24,7 +24,7 @@
<div>
{% if forum.lastPost.id %}
<a href="{{ route('forums.topic', forum.lastPost.topic) }}" class="default">{{ forum.lastPost.subject|slice(0, 30) }}{% if forum.lastPost.subject|length > 30 %}...{% endif %}</a><br>
<time datetime="{{ forum.lastPost.time|date('r') }}">{{ forum.lastPost.time|date('D Y-m-d H:i:s T') }}</time> by {% if forum.lastPost.poster.id %}<a href="{{ route('user.profile', forum.lastPost.poster.id) }}" class="default" style="color: {{ forum.lastPost.poster.colour }}; text-shadow: 0 0 5px {% if forumlastPost.poster.colour != 'inherit' %}{{ forum.lastPost.poster.colour }}{% else %}#222{% endif %};">{{ forum.lastPost.poster.username }}</a>{% else %}[deleted user]{% endif %} <a href="{{ route('forums.post', forum.lastPost.id) }}" class="default fa fa-tag"></a>
<time class="time-ago" datetime="{{ forum.lastPost.time|date('r') }}">{{ forum.lastPost.time|date('D Y-m-d H:i:s T') }}</time> by {% if forum.lastPost.poster.id %}<a href="{{ route('user.profile', forum.lastPost.poster.id) }}" class="default" style="color: {{ forum.lastPost.poster.colour }}; text-shadow: 0 0 5px {% if forumlastPost.poster.colour != 'inherit' %}{{ forum.lastPost.poster.colour }}{% else %}#222{% endif %};">{{ forum.lastPost.poster.username }}</a>{% else %}[deleted user]{% endif %} <a href="{{ route('forums.post', forum.lastPost.id) }}" class="default fa fa-tag"></a>
{% else %}
There are no posts in this forum.<br>&nbsp;
{% endif %}

View file

@ -53,14 +53,14 @@
rText = document.getElementById('previewText'),
lastParsed = Date.now(),
lastKeystroke = Date.now(),
parser = new AJAX(),
postFetch = new AJAX(),
parser = new Sakura.AJAX(),
postFetch = new Sakura.AJAX(),
parserActive = false,
op = {{ topic is defined ? topic.firstPost.id : 0 }},
topicName = "{{ topic is defined ? topic.firstPost.subject : '' }}";
parser.setUrl("{{ route('helper.bbcode.parse') }}");
parser.contentType("application/x-www-form-urlencoded");
parser.SetUrl("{{ route('helper.bbcode.parse') }}");
parser.ContentType("application/x-www-form-urlencoded");
pText.addEventListener("focus", function () {
preview.style.display = null;
@ -89,10 +89,10 @@
} else {
parserActive = true;
parser.setSend({"text":text});
parser.SetSend({"text":text});
parser.addCallback(200, function () {
rText.innerHTML = parser.response();
parser.AddCallback(200, function () {
rText.innerHTML = parser.Response();
var codeBlocks = rText.querySelectorAll("pre code");
@ -105,7 +105,7 @@
parserActive = false;
});
parser.start(HTTPMethods.POST);
parser.Start(Sakura.HTTPMethod.POST);
}
}
}, 1000);
@ -156,10 +156,10 @@
url = "{{ route('forums.post.raw', '1') }}".replace('1', id);
postFetch.setUrl(url);
postFetch.SetUrl(url);
postFetch.addCallback(200, function () {
pText.value = postFetch.response();
postFetch.AddCallback(200, function () {
pText.value = postFetch.Response();
pStopEdit.style.display = null;
pForm.action = "{{ route('forums.post.edit', '1') }}".replace('1', id);
pMode.innerText = 'Editing #' + id;
@ -172,7 +172,7 @@
pText.focus();
});
postFetch.start(HTTPMethods.GET);
postFetch.Start(Sakura.HTTPMethod.GET);
}
function stopEdit() {

View file

@ -22,6 +22,6 @@
{% else %}
[deleted user]
{% endif %} <a href="{{ route('forums.post', topic.lastPost.id) }}" class="default fa fa-tag"></a><br>
<time datetime="{{ topic.lastPost.time|date('r') }}">{{ topic.lastPost.time|date('D Y-m-d H:i:s T') }}</time>
<time class="time-ago" datetime="{{ topic.lastPost.time|date('r') }}">{{ topic.lastPost.time|date('D Y-m-d H:i:s T') }}</time>
</td>
</tr>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title %}Forums / {{ forum.name }}{% endset %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Forums' %}
@ -17,7 +17,7 @@
<td style="text-align: left; border-bottom: 1px solid #9475b2;">
<a href="{{ route('forums.topic', _t.id) }}" class="default">{{ _t.title }}</a>
</td>
<td class="rightAlign" style="border-bottom: 1px solid #9475b2;"><time datetime="{{ _t.lastPost.time|date('r') }}">{{ _t.lastPost.time|date('D Y-m-d H:i:s T') }}</time></td>
<td class="rightAlign" style="border-bottom: 1px solid #9475b2;"><time class="time-ago" datetime="{{ _t.lastPost.time|date('r') }}">{{ _t.lastPost.time|date('D Y-m-d H:i:s T') }}</time></td>
</tr>
{% endfor %}
</table>
@ -36,7 +36,7 @@
by
<a href="{{ route('user.profile', _p.poster.id) }}" class="default"><span style="color: {{ _p.poster.colour }};">{{ _p.poster.username }}</span></a>
</td>
<td class="rightAlign" style="border-bottom: 1px solid #9475b2;"><time datetime="{{ _p.time|date('r') }}">{{ _p.time|date('D Y-m-d H:i:s T') }}</time></td>
<td class="rightAlign" style="border-bottom: 1px solid #9475b2;"><time class="time-ago" datetime="{{ _p.time|date('r') }}">{{ _p.time|date('D Y-m-d H:i:s T') }}</time></td>
</tr>
{% endfor %}
</table>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set forumBackLink %}{{ route('forums.forum', forum.id) }}{% endset %}
@ -97,7 +97,7 @@
{% endif %}
{% if not (post.poster.permission(constant('Sakura\\Perms\\Site::DEACTIVATED')) or post.poster.permission(constant('Sakura\\Perms\\Site::RESTRICTED')) or user.id == post.poster.id) %}
<a class="fa fa-{% if user.isFriends(post.poster.id) == 2 %}heart{% else %}star{% endif %} friend-{{ post.poster.id }}-level" title="You are friends" {% if user.isFriends(post.poster.id) == 0 %}style="display: none;"{% endif %}></a>
<a class="fa fa-user-{% if user.isFriends(post.poster.id) == 0 %}plus{% else %}times{% endif %} forum-friend-toggle friend-{{ post.poster.id }}-toggle" title="{% if user.isFriends(post.poster.id) == 0 %}Add {{ post.poster.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(post.poster.id) == 0 %}addFriend({{ post.poster.id }}){% else %}removeFriend({{ post.poster.id }}){% endif %}"></a>
<a class="fa fa-user-{% if user.isFriends(post.poster.id) == 0 %}plus{% else %}times{% endif %} forum-friend-toggle friend-{{ post.poster.id }}-toggle" title="{% if user.isFriends(post.poster.id) == 0 %}Add {{ post.poster.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="Sakura.Friend.{% if user.isFriends(post.poster.id) == 0 %}Add({{ post.poster.id }}){% else %}Remove({{ post.poster.id }}){% endif %}"></a>
<a class="fa fa-flag" title="Report {{ post.poster.username }}" href="{{ route('user.report', post.poster.id) }}"></a>
{% endif %}
<a class="fa fa-reply" title="Quote this post" href="javascript:void(0);" onclick="quotePost({{ post.id }});"></a>
@ -111,7 +111,7 @@
<a href="#p{{ post.id }}" class="clean">{{ post.subject|slice(0, 50) }}{% if post.subject|length > 50 %}...{% endif %}</a>
</div>
<div class="date">
<a href="{{ route('forums.post', post.id) }}" class="clean">#{{ post.id }} - <time datetime="{{ post.time|date('r') }}">{{ post.time|date('D Y-m-d H:i:s T') }}</time></a>
<a href="{{ route('forums.post', post.id) }}" class="clean">#{{ post.id }} - <time class="time-ago" datetime="{{ post.time|date('r') }}">{{ post.time|date('D Y-m-d H:i:s T') }}</time></a>
</div>
<div class="clear"></div>
</div>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Confirmation' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Information' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Restricted' %}

View file

@ -1 +1 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% block content %}
<div class="content standalone bbcode">

View file

@ -0,0 +1,27 @@
{% extends 'master.twig' %}
{% set title = category ~ ' / ' ~ mode %}
{% block content %}
<div class="content settings messages">
<div class="content-right content-column">
<div class="head">
Navigation
</div>
<div class="right-menu-nav">
{% for name, links in navigation %}
<div>{{ name }}</div>
{% for name,link in links %}
<a href="{{ link }}">{{ name }}</a>
{% endfor %}
{% endfor %}
</div>
</div>
<div class="content-left content-column">
<div class="head">{{ title }}</div>
<div class="settings-explanation">{{ block('description') }}</div>
{{ block('manageContent') }}
</div>
<div class="clear"></div>
</div>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends 'manage/overview/master.twig' %}
{% set mode = 'Index' %}
{% block description %}
<p>A quick overview on what's going on.</p>
{% endblock %}
{% block manageContent %}
<h1>wew</h1>
{% endblock %}

View file

@ -0,0 +1,3 @@
{% extends 'manage/master.twig' %}
{% set category = 'Overview' %}

View file

@ -1,7 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<!-- META -->
<meta charset="utf-8">
<title>{{ title|default(config('general.name')) }}</title>
<meta name="description" content="{{ description|default(config('general.description')) }}">
@ -10,13 +9,13 @@
<meta http-equiv="refresh" content="{{ redirectTimeout ? redirectTimeout : '3' }}; URL={{ redirect }}">
{% endif %}
{{ block('meta') }}
<!-- CSS -->
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" type="text/css" href="/css/libraries.css">
<link rel="stylesheet" type="text/css" href="/css/yuuno.css">
{{ block('css') }}
<!-- JS -->
<script type="text/javascript" src="/js/master.js"></script>
<script type="text/javascript" src="/js/app.js"></script>
<script type="text/javascript" src="/js/yuuno.js"></script>
<script type="text/javascript">
// Create an object so we can access certain settings from remote JavaScript files
@ -35,9 +34,6 @@
"checkLogin": {{ user.isActive ? 'true' : 'false' }}
};
// Set cookie prefix and path
Sakura.cookiePrefix = "{{ config('cookie.prefix') }}";
// Error reporter
window.onerror = function(msg, url, line, col, error) {
notifyUI({
@ -73,7 +69,7 @@
<a class="menu-item avatar" href="{{ route('user.profile', user.id) }}" title="Logged in as {{ user.username }}" style="background-image: url('{{ route('file.avatar', user.id) }}'); width: auto; color: {{ user.colour }}; border-color: {{ user.colour }}; font-weight: 700;"></a>
{#<a class="menu-item fa-envelope" href="#" title="Messages"></a>#}
{% if user.permission(constant('Sakura\\Perms\\Manage::USE_MANAGE'), constant('Sakura\\Perms::MANAGE')) %}
<a class="menu-item fa-gavel" href="#" title="Manage"></a>
<a class="menu-item fa-gavel" href="{{ route('manage.index') }}" title="Manage"></a>
{% endif %}
<a class="menu-item fa-cogs" href="{{ route('settings.index') }}" title="Settings"></a>
<a class="menu-item fa-sign-out" href="{{ route('auth.logout') }}?s={{ session_id() }}" title="Logout" id="headerLogoutLink"></a>
@ -164,175 +160,26 @@
</div>
</div>
<script type="text/javascript">
// Parse time elements
var timeElems = document.getElementsByTagName('time');
Sakura.Config.Set({
Revision: {{ constant('SAKURA_VERSION') }},
UserNameMinLength: {{ config('user.name_min') }},
UserNameMaxLength: {{ config('user.name_max') }},
PasswordMinEntropy: {{ config('user.pass_min_entropy') }},
LoggedIn: {{ user.isActive ? 'true' : 'false' }},
SessionId: "{{ session_id() }}",
});
// Iterate over them
for (var timeElem in timeElems) {
console.log(timeElems[timeElem].dateTime);
// Attempt to parse it
var parsed = Date.parse(timeElems[timeElem].dateTime);
Yuuno.Main.Startup();
// If it can be parsed do DOM edits
if (!isNaN(parsed)) {
timeElems[timeElem].title = timeElems[timeElem].innerText;
timeElems[timeElem].innerText = Sakura.timeElapsed(Math.floor(parsed / 1000));
}
}
notifyRequest('{{ session_id() }}');
setInterval(function() {
notifyRequest('{{ session_id() }}');
}, 60000);
var friendClient = new AJAX();
friendClient.contentType("application/x-www-form-urlencoded");
function addFriend(id) {
var url = "{{ route('friends.add', 1) }}".replace(1, id);
friendMode(url, id);
}
function removeFriend(id) {
var url = "{{ route('friends.remove', 1) }}".replace(1, id);
friendMode(url, id);
}
function friendMode(url, id) {
var level = document.querySelectorAll('.friend-' + id + '-level'),
toggle = document.querySelectorAll('.friend-' + id + '-toggle');
friendClient.setUrl(url);
friendClient.setSend({"session":"{{ session_id() }}"});
friendClient.addCallback(200, function () {
var resp = JSON.parse(friendClient.response());
friendClient.setRawSend("");
if (resp.error) {
ajaxBusyView(true, resp.error, 'fail');
setTimeout(function () {
ajaxBusyView(false);
}, 1500);
} else {
ajaxBusyView(true, resp.message, 'ok');
location.reload();
}
});
friendClient.start(HTTPMethods.POST);
}
</script>
{% if config('dev.show_changelog', true) and stats %}
<script type="text/javascript">
// Column colours for actions
var changelogColours = [
'inherit', // Unknown
'#2A2', // Add
'#2AA', // Update
'#2AA', // Fix
'#A22', // Remove
'#62C', // Export
'#C44' // Revert
];
function yuunoCreateChangelog() {
// Grab the index panel
var _panel = document.getElementById('indexPanel');
// Create the head container
var _cltitle = document.createElement('div');
_cltitle.className = 'head';
_cltitle.style.marginBottom = '1px';
// Create a link
var _cllink = document.createElement('a');
_cllink.className = 'underline';
_cllink.target = '_blank';
_cllink.href = 'https://sakura.flash.moe/#r{{ constant('SAKURA_VERSION') }}';
// Append everything
_cllink.appendChild(document.createTextNode('Changelog'));
_cltitle.appendChild(_cllink);
_panel.appendChild(_cltitle);
// Create changelog table
var _cltable = document.createElement('table');
_cltable.className = 'panelTable';
_cltable.style.borderSpacing = '0 1px';
_cltable.id = 'yuunoChangelogTable';
// Append it to indexPanel
indexPanel.appendChild(_cltable);
}
function yuunoAddToChangelog(object) {
_cltable = document.getElementById('yuunoChangelogTable');
// Create header
var _hr = document.createElement('tr');
var _hri = document.createElement('td');
// Set data
_hri.appendChild(document.createTextNode(object.date));
_hri.style.background = '#9475b2';
_hri.style.color = '#306';
_hri.style.fontWeight = '700';
_hri.style.fontSize = '1.2em';
_hri.setAttribute('colspan', '2');
// Append
_hr.appendChild(_hri);
_cltable.appendChild(_hr);
for (var _e in object.changes) {
// Reassign _e
_e = object.changes[_e];
// Create elements
var _clr = document.createElement('tr');
var _clca = document.createElement('td');
var _clcm = document.createElement('td');
// Set data
_clca.appendChild(document.createTextNode(_e.action.name));
_clca.style.background = changelogColours[_e.action.id];
_clca.style.borderBottom = '1px solid ' + changelogColours[_e.action.id];
_clcm.style.borderBottom = '1px solid ' + changelogColours[_e.action.id];
_clcm.appendChild(document.createTextNode(_e.message));
// Append
_clr.appendChild(_clca);
_clr.appendChild(_clcm);
_cltable.appendChild(_clr);
}
}
window.addEventListener("load", function () {
yuunoCreateChangelog();
var changelog = {},
fetch = 0,
clg = new AJAX();
clg.addCallback(200, function () {
yuunoAddToChangelog(JSON.parse(clg.response()));
if (fetch < 2) {
fetch++;
clg.setUrl('https://sakura.flash.moe/api.php/' + fetch);
clg.start(HTTPMethods.GET);
}
});
clg.setUrl('https://sakura.flash.moe/api.php/' + fetch);
clg.start(HTTPMethods.GET);
});
</script>
Sakura.Changelog.Build(new Sakura.DOM('indexPanel', Sakura.DOMSelector.ID));
{% endif %}
notifyRequest(Sakura.Config.SessionId);
setInterval(function() {
notifyRequest(Sakura.Config.SessionId);
}, 60000);
</script>
</body>
</html>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'You are banned!' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Frequently Asked Questions' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% block content %}
<div class="content homepage">

View file

@ -1 +1 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Search' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title %}{{ page.category }} / {{ page.mode }}{% endset %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'News' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = post.title %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = 'Purchase complete!' %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title %}Support {{ config('general.name') }}{% endset %}

View file

@ -12,7 +12,7 @@
{% block settingsContent %}
<form enctype="multipart/form-data" method="post" action="{{ route('settings.account.username') }}">
<h1 class="stylised" style="text-align: center; margin-top: 10px;{% if not eligible %} color: #c44;{% endif %}">You are {% if not eligible %}not {% endif %}eligible for a name change.</h1>
<h3 style="text-align: center;">{% if user.getUsernameHistory %}Your last name change was <time datetime="{{ user.getUsernameHistory[0]['change_time']|date('r') }}">{{ user.getUsernameHistory[0]['change_time']|date('D Y-m-d H:i:s T') }}</time>.{% else %}This is your first username change.{% endif %}</h3>
<h3 style="text-align: center;">{% if user.getUsernameHistory %}Your last name change was <time class="time-ago" datetime="{{ user.getUsernameHistory[0]['change_time']|date('r') }}">{{ user.getUsernameHistory[0]['change_time']|date('D Y-m-d H:i:s T') }}</time>.{% else %}This is your first username change.{% endif %}</h3>
{% if eligible %}
<div class="profile-field">
<div><h2>Username</h2></div>

View file

@ -26,7 +26,7 @@
{{ s.user_agent }}
</td>
<td>
<time datetime="{{ s.session_start|date('r') }}">{{ s.session_start|date('D Y-m-d H:i:s T') }}</time>
<time class="time-ago" datetime="{{ s.session_start|date('r') }}">{{ s.session_start|date('D Y-m-d H:i:s T') }}</time>
</td>
<td style="width: 90px;">
<form method="post" action="{{ route('settings.advanced.sessions') }}">

View file

@ -1,11 +1,11 @@
<script type="text/javascript">
var parser = new AJAX(),
var parser = new Sakura.AJAX(),
textMax = {{ maxLength }},
form = document.getElementById("settingsEditor"),
preview = document.getElementById("settingsPreview");
parser.setUrl("{{ route('helper.bbcode.parse') }}");
parser.contentType("application/x-www-form-urlencoded");
parser.SetUrl("{{ route('helper.bbcode.parse') }}");
parser.ContentType("application/x-www-form-urlencoded");
function settingsPreview() {
var text = form.value;
@ -15,10 +15,10 @@
} else if (text.length > textMax) {
preview.innerHTML = "<span style='color: red;'>Too long!</span>";
} else {
parser.setSend({"text":text});
parser.SetSend({"text":text});
parser.addCallback(200, function () {
preview.innerHTML = parser.response();
parser.AddCallback(200, function () {
preview.innerHTML = parser.Response();
var codeBlocks = preview.querySelectorAll("pre code");
@ -29,7 +29,7 @@
}
});
parser.start(HTTPMethods.POST);
parser.Start(Sakura.HTTPMethod.POST);
}
}
</script>

View file

@ -21,7 +21,7 @@
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="remove fill fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<a class="remove fill fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="Sakura.Friend.Remove({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>

View file

@ -21,8 +21,8 @@
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="add fa fa-check" title="Add friend" href="javascript:void(0);" onclick="addFriend({{ friend.id }});"></a>
<a class="remove fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<a class="add fa fa-check" title="Add friend" href="javascript:void(0);" onclick="Sakura.Friend.Add({{ friend.id }});"></a>
<a class="remove fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="Sakura.Friend.Remove({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set title = category ~ ' / ' ~ mode %}

View file

@ -41,7 +41,7 @@
</div>
</div>
<div class="notif-hist-time">
<time datetime="{{ alert.time|date('r') }}">{{ alert.time|date('D Y-m-d H:i:s T') }}</time>
<time class="time-ago" datetime="{{ alert.time|date('r') }}">{{ alert.time|date('D Y-m-d H:i:s T') }}</time>
</div>
</div>
<div class="clear"></div>

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set sorts = ['boxes', 'rectangles', 'list'] %}
{% set sort = get.sort in sorts ? get.sort : sorts[0] %}
@ -80,10 +80,10 @@
<a href="{{ route('user.profile', user.id) }}" class="default" style="font-weight: bold; color: {{ user.colour }}; text-shadow: 0 0 5px {{ user.colour }};">{{ user.username }}</a>
</td>
<td>
<time datetime="{{ user.registered|date('r') }}">{{ user.registered|date('D Y-m-d H:i:s T') }}</time>
<time class="time-ago" datetime="{{ user.registered|date('r') }}">{{ user.registered|date('D Y-m-d H:i:s T') }}</time>
</td>
<td>
{% if user.lastOnline == 0 %}<i>Never logged in.</i>{% else %}<time datetime="{{ user.lastOnline|date('r') }}">{{ user.lastOnline|date('D Y-m-d H:i:s T') }}</time>{% endif %}
{% if user.lastOnline == 0 %}<i>Never logged in.</i>{% else %}<time class="time-ago" datetime="{{ user.lastOnline|date('r') }}">{{ user.lastOnline|date('D Y-m-d H:i:s T') }}</time>{% endif %}
</td>
<td>
{{ user.title }}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% set profileHidden = profile.permission(constant('Sakura\\Perms\\Site::DEACTIVATED')) or (profile.permission(constant('Sakura\\Perms\\Site::RESTRICTED')) and (user.id != profile.id and not user.permission(constant('Sakura\\Perms\\Manage::USE_MANAGE'), constant('Sakura\\Perms::MANAGE')))) %}
@ -84,12 +84,12 @@
{% if profile.isPremium %}<img src="/images/tenshi.png" alt="Tenshi" style="vertical-align: middle;"> {% endif %}<img src="/images/flags/{{ profile.country|lower }}.png" alt="{{ profile.country }}" style="vertical-align: middle;" title="{{ profile.country(true) }}"> <span style="font-size: .8em;">{{ profile.title }}</span>
</div>
<div class="new-profile-dates">
<b>Joined</b> <time datetime="{{ profile.registered|date('r') }}">{{ profile.registered|date('D Y-m-d H:i:s T') }}</time>
<b>Joined</b> <time class="time-ago" datetime="{{ profile.registered|date('r') }}">{{ profile.registered|date('D Y-m-d H:i:s T') }}</time>
<br>
{% if profile.lastOnline < 1 %}
<b>{{ profile.username }} hasn't logged in yet.</b>
{% else %}
<b>Last online</b> <time datetime="{{ profile.lastOnline|date('r') }}">{{ profile.lastOnline|date('D Y-m-d H:i:s T') }}</time>
<b>Last online</b> <time class="time-ago" datetime="{{ profile.lastOnline|date('r') }}">{{ profile.lastOnline|date('D Y-m-d H:i:s T') }}</time>
{% endif %}
{% if profile.birthday != '0000-00-00' and profile.birthday|split('-')[0] > 0 %}
<br><b>Age</b> <span title="{{ profile.birthday }}">{{ profile.birthday(true) }} years old</span>&nbsp;
@ -114,7 +114,7 @@
<a class="fa fa-pencil-square-o" title="Edit your profile" href="{{ route('settings.general.profile') }}"></a>
{% else %}
{% if user.isFriends(profile.id) != 0 %}<a class="fa fa-{% if user.isFriends(profile.id) == 2 %}heart{% else %}star{% endif %}" title="You are friends"></a>{% endif %}
<a class="fa fa-user-{% if user.isFriends(profile.id) == 0 %}plus{% else %}times{% endif %}" title="{% if user.isFriends(profile.id) == 0 %}Add {{ profile.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(profile.id) == 0 %}addFriend({{ profile.id }}){% else %}removeFriend({{ profile.id }}){% endif %}"></a>
<a class="fa fa-user-{% if user.isFriends(profile.id) == 0 %}plus{% else %}times{% endif %}" title="{% if user.isFriends(profile.id) == 0 %}Add {{ profile.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="Sakura.Friend.{% if user.isFriends(profile.id) == 0 %}Add({{ profile.id }}){% else %}Remove({{ profile.id }}){% endif %}"></a>
<a class="fa fa-exclamation-circle" title="Report {{ profile.username }}" href="{{ route('user.report', profile.id) }}"></a>
{% endif %}
{% if user.permission(constant('Sakura\\Perms\\Manage::CAN_RESTRICT_USERS'), constant('Sakura\\Perms::MANAGE')) %}

View file

@ -1,4 +1,4 @@
{% extends 'global/master.twig' %}
{% extends 'master.twig' %}
{% block content %}
<h1 class="stylised" style="text-align: center; margin: 2em auto;">I'll actually make reporting a thing, someday...</h1>

View file

@ -75,8 +75,8 @@ Router::group(['before' => 'maintenance'], function () {
'chat' => 'chat.redirect',
//'irc' => 'chat.irc',
'feedback' => 'forums.index',
//'mcp' => 'manage.index',
//'mcptest' => 'manage.index',
'mcp' => 'manage.index',
'mcptest' => 'manage.index',
//'report' => 'report.something',
//'osu' => 'eventual link to flashii team',
//'filehost' => '???',
@ -120,12 +120,12 @@ Router::group(['before' => 'maintenance'], function () {
Router::group(['prefix' => 'forum'], function () {
// Post
Router::group(['prefix' => 'post'], function () {
Router::get('/{id:i}', 'ForumController@post', 'forums.post');
Router::get('/{id:i}', 'Forum.PostController@find', 'forums.post');
Router::group(['before' => 'loginCheck'], function () {
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::get('/{id:i}/raw', 'Forum.PostController@raw', 'forums.post.raw');
Router::get('/{id:i}/delete', 'Forum.PostController@delete', 'forums.post.delete');
Router::post('/{id:i}/delete', 'Forum.PostController@delete', 'forums.post.delete');
Router::post('/{id:i}/edit', 'Forum.PostController@edit', 'forums.post.edit');
});
});
@ -142,12 +142,12 @@ Router::group(['before' => 'maintenance'], function () {
});
// Forum
Router::get('/', 'ForumController@index', 'forums.index');
Router::get('/{id:i}', 'ForumController@forum', 'forums.forum');
Router::get('/', 'Forum.ForumController@index', 'forums.index');
Router::get('/{id:i}', 'Forum.ForumController@forum', 'forums.forum');
Router::group(['before' => 'loginCheck'], function () {
Router::get('/{id:i}/mark', 'ForumController@markForumRead', 'forums.mark');
Router::get('/{id:i}/new', 'ForumController@createTopic', 'forums.new');
Router::post('/{id:i}/new', 'ForumController@createTopic', 'forums.new');
Router::get('/{id:i}/mark', 'Forum.ForumController@markRead', 'forums.mark');
Router::get('/{id:i}/new', 'Forum.TopicController@create', 'forums.new');
Router::post('/{id:i}/new', 'Forum.TopicController@create', 'forums.new');
});
});
@ -308,16 +308,26 @@ Router::group(['before' => 'maintenance'], function () {
});
});
// Settings
Router::group(['prefix' => 'manage', 'before' => 'loginCheck'], function () {
Router::get('/', function () {
$route = Router::route('manage.overview');
return header("Location: {$route}");
}, 'manage.index');
// Overview section
Router::group(['prefix' => 'overview'], function () {
Router::get('/', function () {
$route = Router::route('manage.overview.index');
return header("Location: {$route}");
}, 'manage.overview');
Router::get('/index', 'Manage.OverviewController@index', 'manage.overview.index');
Router::get('/data', 'Manage.OverviewController@data', 'manage.overview.data');
});
});
// Management
/*
* General
* - Dashboard
* - Info pages (possibly deprecate with wiki)
* Configuration
* - General
* - Files
* - User
* - Mail
* Forums
* - Manage
* - Settings