Selasa 19 November 2019

This commit is contained in:
Damillora 2019-11-19 10:36:39 +07:00
parent 7803ea548d
commit 678dadb1ab
37 changed files with 13080 additions and 95 deletions

View File

@ -0,0 +1,41 @@
<?php
namespace Application\Controllers;
use Application\HTTP\Request;
use Application\HTTP\Response;
use Application\Services\ServiceContainer;
use Application\Models\Category;
use Application\Models\Thread;
use Application\Models\UserAction;
use Application\Foundations\QueryBuilder;
class ApiController {
public function __construct() {
}
public function categories(Request $request, Response $response) {
$bans = [];
if(ServiceContainer::Authentication()->isLoggedIn()) {
$where = new QueryBuilder();
$where = $where->where('user_id',ServiceContainer::Session()->get('user_id'))->where('expired_at','>',date('Y-m-d H:i:s'))->where('action_type','ban')->orderBy('expired_at','desc');
$actions = UserAction::select($where);
$bans = array_map(function($action) {
return (int)$action->category_id;
}, $actions);
}
$where = new QueryBuilder();
$where = $where->where('group_id',$request->id);
if(count($bans) > 0) {
$where = $where->whereNotIn('id',$bans);
}
$categories = Category::select($where);
return $response->json()->data($categories);
}
public function threads(Request $request, Response $response) {
$where = new QueryBuilder();
$where = $where->where('category_id',$request->id);
$threads = Thread::select($where);
return $response->json()->data($threads);
}
}

View File

@ -8,6 +8,7 @@ use Application\Foundations\QueryBuilder;
use Application\Foundations\MailBuilder;
use Application\Models\User;
use Application\Models\UserConfirmation;
use Application\Models\UserChange;
class AuthController {
public function __construct() {
@ -17,7 +18,7 @@ class AuthController {
if(ServiceContainer::Authentication()->isLoggedIn()) {
return $response->redirect('/');
}
return $response->view('sign-up');
return $response->view('auth/sign-up');
}
public function create_user(Request $request, Response $response) {
if($request->email == "") {
@ -45,6 +46,14 @@ class AuthController {
[ 'errors' => 'Password and confirm password must be the same' ]
);
}
$query = new QueryBuilder();
$query = $query->select('id')->from('user')->where('username',$request->username)->build();
$result = ServiceContainer::Database()->select($query);
if(count($result) > 0) {
return $response->redirect("/signup")->with(
[ 'errors' => 'Username is already taken' ]
);
}
ServiceContainer::Session()->unset('errors');
$data = User::create([
'id' => null,
@ -55,6 +64,7 @@ class AuthController {
'avatar_path' => '',
'password' => password_hash($request->password,PASSWORD_DEFAULT),
'is_confirmed' => 0,
'role' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
@ -72,19 +82,17 @@ class AuthController {
$email->from("metaforums@nanao.moe")->to($data->email)->subject("Complete your registration on Metaforums")->body($body);
ServiceContainer::Email()->send($email);
if($confirmator != null) {
return $response->redirect('/signup/success')->with(
return $response->redirect('/login')->with(
[ 'signup-email' => $request->email ],
);
}
}
}
public function sign_up_success(Request $request, Response $response) {
return $response->view('sign-up-success');
}
public function sign_up_confirm(Request $request, Response $response) {
$confirm = UserConfirmation::find($request->queryString());
if(isset($confirm)) {
if(isset($confirm) && strtotime($confirm->best_before) > time() ) {
$id = $confirm->user_id;
$user = User::find($id);
$user->update([ 'is_confirmed' => 1 ]);
$confirm->delete();
@ -95,39 +103,102 @@ class AuthController {
if(ServiceContainer::Authentication()->isLoggedIn()) {
return $response->redirect('/');
}
return $response->view('login');
return $response->view('auth/login');
}
public function login_check(Request $request, Response $response) {
if ($request->username == "") {
return $response->redirect("/login")->with(
[ 'errors' => 'Username must not be empty' ]
);
} else if ($request->password == "") {
return $response->redirect("/login")->with(
[ 'errors' => 'Password must not be empty' ]
);
}
$query = new QueryBuilder();
$query = $query->select('id,password')->from('user')->where('username',$request->username)->where('is_confirmed',1)->build();
$result = ServiceContainer::Database()->select($query);
if(count($result) == 0) {
$query = $query->where('username',$request->username)->orWhere('email',$request->username);
$result = User::selectOne($query);
if($result == null) {
if(filter_var($request->username,FILTER_VALIDATE_EMAIL)) {
return $response->redirect("/login")->with(
[ 'errors' => 'Wrong username or password' ]
[ 'errors' => 'Email is not associated with an account' ]
);
} else {
$password = $result[0]["password"];
return $response->redirect("/login")->with(
[ 'errors' => 'Username does not exist' ]
);
}
} else {
$password = $result->password;
$verify = password_verify($request->password,$password);
if(!$verify) {
return $response->redirect("/login")->with(
[ 'errors' => 'Wrong username or password' ]
[ 'errors' => 'Invalid password' ]
);
}
}
ServiceContainer::Session()->set('user_id',$result[0]['id']);
$result->update([ 'logged_in' => 1, 'last_login' => date('Y-m-d H:i:s') ]);
ServiceContainer::Session()->unset('errors');
ServiceContainer::Session()->set('user_id',$result->id);
return $response->redirect('/');
}
public function logout(Request $request, Response $response) {
$user = User::find(ServiceContainer::Session()->get('user_id'));
$user->update([ 'logged_in' => 0]);
ServiceContainer::Session()->destroy();
return $response->redirect('/login');
}
public function forget_password(Request $request, Response $response) {
if(ServiceContainer::Authentication()->isLoggedIn()) {
return $response->redirect("/");
}
return $response->view('auth/forgot');
}
public function forget_password_confirm(Request $request, Response $response) {
$query = new QueryBuilder();
$query = $query->select('id,username,email')->from('user')->where('email',$request->email)->where('is_confirmed',1)->build();
$result = ServiceContainer::Database()->select($query);
if(count($result) > 0) {
$confirmator = UserChange::create([
'user_id' => $result[0]["id"],
'action_type' => 'password_reset',
'confirm_key' => hash('sha256',$result[0]['username'].time()),
'best_before' => date('Y-m-d H:i:s', strtotime('+6 hours', time())),
]);
$email = new MailBuilder();
$body = "I heard you forgot your password.\n";
$body .= "To reset your password, use the URL below:\n\n";
$body .= $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'].'/login/reset?'.$confirmator->confirm_key;
$email->from("metaforums@nanao.moe")->to($result[0]['email'])->subject("Someone asked to reset your password")->body($body);
ServiceContainer::Email()->send($email);
}
return $response->redirect("/login/forget")->with([ 'forget_message' => 'We have sent a reset password link to the provided e-mail. If there is an account associated with the e-mail, the e-mail will be received in the inbox.' ]);
}
public function reset_password(Request $request, Response $response) {
if(ServiceContainer::Authentication()->isLoggedIn()) {
return $response->redirect("/");
}
$where = new QueryBuilder();
$where->where('confirm_key',$request->queryString())->where('action_type','password_reset')->where("best_before",">",date('Y-m-d H:i:s'));
$confirmator = UserChange::selectOne($where);
if(!isset($confirmator)) {
return $response->redirect("/");
}
return $response->view('auth/reset', [ 'key' => $request->queryString() ]);
}
public function reset_password_confirm(Request $request, Response $response) {
if(ServiceContainer::Authentication()->isLoggedIn()) {
return $response->redirect("/");
}
$where = new QueryBuilder();
$where->where('confirm_key',$request->confirm_key)->where('action_type','password_reset')->where("best_before",">",date('Y-m-d H:i:s'));
$confirmator = UserChange::selectOne($where);
if(!isset($confirmator)) {
return $response->redirect("/");
}
if (strlen($request->password) < 8) {
return $response->redirect("/login/reset?".$request->confirm_key)->with(
[ 'errors' => 'Password must be at least 8 characters' ]
);
} else if ($request->password != $request->confirmpassword) {
return $response->redirect("/login/reset?".$request->confirm_key)->with(
[ 'errors' => 'Password and confirm password must be the same' ]
);
}
$user = User::find($confirmator->user_id);
$user->update(['password' => password_hash($request->password,PASSWORD_DEFAULT) ]);
$confirmator->delete();
return $response->redirect("/login");
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Application\Controllers;
use Application\HTTP\Request;
use Application\HTTP\Response;
use Application\Services\ServiceContainer;
use Application\Models\Category;
use Application\Models\Post;
use Application\Models\Thread;
use Application\Models\UserAction;
use Application\Foundations\QueryBuilder;
class ForumThreadController {
public function __construct() {
}
public function forum(Request $request, Response $response) {
$thread = Thread::find($request->id);
return $response->view('thread', [ 'thread' => $thread ] );
}
public function process(Request $request, Response $response) {
$user = ServiceContainer::Authentication()->user();
$eligible = true;
if($user->is_confirmed == 0) {
$query = new QueryBuilder();
$query = $query->where("user_id",$user->id)->where("created_at",">",date("Y-m-d H:i:s",strtotime(" - 24 hours")));
$posts = Post::select($query);
if(count($posts) < 0) {
$eligible = false;
}
}
$category = Category::find($request->category);
$thread = Thread::find($request->thread);
$reply = Post::find($request->reply);
$edit = Post::find($request->edit);
if(isset($edit)) {
$edit->update([ 'post' => $request->content, 'updated_at' => date("Y-m-d H:i:s") ]);
return $response->redirect('/');
} else if (isset($thread) && isset($reply)) {
$title = $reply->title;
if(strpos($reply->title,"Re: ") != 0) {
$title .= "Re: ".$reply->title;
}
if($eligible) {
Post::create([
'thread_id' => $thread->id,
'user_id' => $user->id,
'title' => $title,
'post' => $request->content,
'created_at' => date("Y-m-d H:i:s"),
'updated_at' => date("Y-m-d H:i:s"),
]);
}
return $response->redirect('/');
} else if (isset($category)) {
$title = $request->title;
$thread = Thread::create([
'category_id' => $category->id,
'title' => $title,
'author' => $user->id,
'created_at' => date("Y-m-d H:i:s"),
'updated_at' => date("Y-m-d H:i:s"),
]);
Post::create([
'thread_id' => $thread->id,
'user_id' => $user->id,
'title' => $title,
'post' => $request->content,
'created_at' => date("Y-m-d H:i:s"),
'updated_at' => date("Y-m-d H:i:s"),
]);
return $response->redirect('/');
}
}
public function editor(Request $request, Response $response) {
$title = "";
$category = Category::find($request->category);
$thread = Thread::find($request->thread);
$reply = Post::find($request->reply);
$edit = Post::find($request->edit);
if(isset($edit)) {
$title = "Editing post";
} else if (isset($thread) && isset($reply) && $thread->main_post->id == $reply->id ) {
$title = "Replying to Main Post";
} else if (isset($thread) && isset($reply)) {
$title = "Replying to ".$reply->user()->username;
} else if (isset($category)) {
$title = "Creating Thread to ".$category->category_name;
}
return $response->view('editor', [ 'title' => $title, 'category' => $request->category, 'thread' => $request->thread, 'edit' => $request->edit, 'reply' => $request->reply, 'edit_post' => $edit ] );
}
}

View File

@ -4,12 +4,28 @@ namespace Application\Controllers;
use Application\HTTP\Request;
use Application\HTTP\Response;
use Application\Services\ServiceContainer;
use Application\Models\Category;
use Application\Models\Group;
use Application\Models\Thread;
class IndexController {
public function __construct() {
}
public function index(Request $request, Response $response) {
return $response->view('index');
$groups = Group::all();
$group = null;
$category = null;
$thread = null;
if(isset($request->group)) {
$group = Group::find($request->group);
}
if(isset($request->category)) {
$category = Category::find($request->category);
}
if(isset($request->thread)) {
$thread = Thread::find($request->thread);
}
return $response->view('index', ['groups' => $groups, 'group' => $group, 'category' => $category, 'thread' => $thread ] );
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Application\Foundations;
class DateHelper {
public function elapsedString($date) {
$unixTimestamp = strtotime($date);
$now = time();
$dateDiff = $now - $unixTimestamp;
$measure = "";
$ago = "";
if($dateDiff > 0) {
$ago .= " ago";
} else if($dateDiff < 0) {
$ago .= " later";
}
$dateDiff = abs($dateDiff);
$seconds = $dateDiff;
$minutes = intval($dateDiff / 60);
$hours = intval($dateDiff / 3600);
$days = intval($dateDiff / 86400);
$years = intval($dateDiff / (365 * 86400));
if($years > 0) {
$measure = $years." years";
} else if($days > 0) {
$measure = $days." days";
} else if($hours > 0) {
$measure = $hours." hours";
} else if($minutes > 0) {
$measure = $minutes." minutes";
} else if($seconds > 0) {
$measure = $seconds." seconds";
}
return $measure.$ago;
}
}

View File

@ -3,7 +3,7 @@ namespace Application\Foundations;
use Application\Services\ServiceContainer;
class Model {
class Model implements \JsonSerializable {
public $attributes;
protected $primary_key = 'id';
@ -21,7 +21,7 @@ class Model {
$data['id'] = $result;
}
$inst = new $calling_class();
$inst->hydrate($result);
$inst->hydrate($data);
return $inst;
}
public static function find($key) {
@ -36,6 +36,38 @@ class Model {
$inst->hydrate($result[0]);
return $inst;
}
public static function select($where_query) {
$calling_class = get_called_class();
$class = explode('\\',get_called_class());
$tablename = strtolower($class[count($class)-1]);
$query = new QueryBuilder();
$query = $query->select('*')->from($tablename)->build();
$query .= $where_query->build();
$result = ServiceContainer::Database()->select($query);
if(count($result) == 0) return [];
foreach($result as $key => $val) {
$inst = new $calling_class();
$inst->hydrate($result[$key]);
$result[$key] = $inst;
}
return $result;
}
public static function selectOne($where_query) {
$calling_class = get_called_class();
$class = explode('\\',get_called_class());
$tablename = strtolower($class[count($class)-1]);
$query = new QueryBuilder();
$query = $query->select('*')->from($tablename)->build();
$query .= $where_query->build();
$result = ServiceContainer::Database()->select($query);
if(count($result) == 0) return null;
$inst = new $calling_class();
$inst->hydrate($result[0]);
return $inst;
}
public static function all() {
return self::select(new QueryBuilder());
}
public function update($key) {
$calling_class = get_called_class();
$class = explode('\\',get_called_class());
@ -61,11 +93,26 @@ class Model {
}
}
function __get($prop) {
$methodName = $prop."_attribute";
if(method_exists($this,$methodName)) {
return $this->$methodName();
}
return $this->attributes[$prop];
}
function __set($prop, $val) {
$this->attributes[$prop] = $val;
}
public function jsonSerialize() {
$data = $this->attributes;
$attr = get_class_methods($this);
$attr = array_filter($attr, function ($var) {
return strpos($var, "_attribute") !== false;
});
foreach($attr as $attr) {
$attrName = substr($attr,0,strlen($attr) - strlen("_attribute"));
$data[$attrName] = $this->$attr();
}
return $data;
}
}

View File

@ -4,6 +4,7 @@ namespace Application\Foundations;
class QueryBuilder {
private $query = "";
private $where = "";
private $misc = "";
public function select($fields) {
if(!is_array($fields)) {
$this->query .= "SELECT ".$fields;
@ -12,6 +13,10 @@ class QueryBuilder {
}
return $this;
}
public function orderBy($column, $order = 'asc') {
$this->misc .= " ORDER BY ".$column." ".strtoupper($order);
return $this;
}
public function delete() {
$this->query .= "DELETE";
return $this;
@ -32,7 +37,7 @@ class QueryBuilder {
}
public function from($table) {
// TODO: SQL injection
$this->query .= " FROM ".$table;
$this->query .= " FROM `".$table."`";
return $this;
}
public function where($a, $b, $c = null) {
@ -56,6 +61,26 @@ class QueryBuilder {
}
return $this;
}
public function whereIn($a, $b) {
$field = $a;
$value = SQLHelper::encode_list($b);
if($this->where == "") {
$this->where .= " WHERE ".$field." IN (".implode(",",$value).")";
} else {
$this->where .= " AND ".$field." IN (".implode(",",$value).")";
}
return $this;
}
public function whereNotIn($a, $b) {
$field = $a;
$value = SQLHelper::encode_list($b);
if($this->where == "") {
$this->where .= " WHERE ".$field." NOT IN (".implode(",",$value).")";
} else {
$this->where .= " AND ".$field." NOT IN (".implode(",",$value).")";
}
return $this;
}
public function orWhere($a, $b, $c = null) {
$field = "";
$value = "";
@ -69,6 +94,7 @@ class QueryBuilder {
$value = $c;
$operator = $b;
}
$value = SQLHelper::encode_literal($value);
if($this->where == "") {
$this->where .= " WHERE ".$field." ".$operator." ".$value;
} else {
@ -77,6 +103,6 @@ class QueryBuilder {
return $this;
}
public function build() {
return $this->query.$this->where;
return $this->query.$this->where.$this->misc;
}
}

View File

@ -12,6 +12,7 @@ class Request {
return $this->query;
}
function __get($prop) {
if(!array_key_exists($prop,$this->data)) return "";
return $this->data[$prop];
}
function __set($prop, $val) {

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class Category extends DBModel {
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class Group extends DBModel {
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class ModeratorCategory extends DBModel {
}

View File

@ -0,0 +1,27 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
use Application\Foundations\DateHelper;
use Application\Foundations\QueryBuilder;
use Application\Services\ServiceContainer;
class Post extends DBModel {
public function user() {
$user = User::Find($this->user_id);
return $user;
}
public function elapsed_created_attribute() {
return DateHelper::elapsedString($this->created_at);
}
public function favorites_attribute() {
$query = new QueryBuilder();
$query = $query->select("COUNT(user_id) AS count")->from("userfavorite")->where("post_id",$this->id)->build();
$result = ServiceContainer::Database()->select($query);
return $result[0]["count"];
}
public function is_main() {
$id = Thread::find($this->thread_id)->main_post->id;
return ($id == $this->id);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
use Application\Foundations\QueryBuilder;
use Application\Foundations\DateHelper;
use Application\Services\ServiceContainer;
class Thread extends DBModel {
public function author_model_attribute() {
return User::find($this->author);
}
public function post_count_attribute() {
$query = new QueryBuilder();
$query = $query->select("COUNT(id) AS count")->from("post")->where("thread_id",$this->id)->build();
$result = ServiceContainer::Database()->select($query);
return $result[0]["count"];
}
public function elapsed_created_attribute() {
return DateHelper::elapsedString($this->created_at);
}
public function last_reply_attribute() {
$query = new QueryBuilder();
$query = $query->where('thread_id',$this->id)->orderBy('created_at','desc');
$post = Post::selectOne($query);
return DateHelper::elapsedString($post->created_at);
}
public function main_post_attribute() {
$query = new QueryBuilder();
$query = $query->where('thread_id',$this->id)->orderBy('created_at','asc');
return Post::selectOne($query);
}
public function is_hot_attribute() {
$query = new QueryBuilder();
$query = $query->where('thread_id',$this->id)->where('created_at','>',date('Y-m-d H:i:s',strtotime(' - 5 minutes')))->orderBy('created_at','desc');
$post = Post::select($query);
return count($post) > 10;
}
public function posts() {
$query = new QueryBuilder();
$query = $query->where('thread_id',$this->id);
$post = Post::select($query);
return $post;
}
public function category() {
$category = Category::Find($this->category_id);
return $category;
}
}

View File

@ -2,7 +2,46 @@
namespace Application\Models;
use Application\Foundations\Model as DBModel;
use Application\Foundations\QueryBuilder;
use Application\Services\ServiceContainer;
class User extends DBModel {
public function is_moderator_attribute() {
return ($this->role >= 2500);
}
public function is_admin_attribute() {
return ($this->role >= 100000);
}
public function role_string_attribute() {
if($this->is_admin) {
return "Site Admin";
} else if($this->is_moderator) {
return "Moderator";
}
return "User";
}
public function post_count_attribute() {
$query = new QueryBuilder();
$query = $query->select("COUNT(id) AS count")->from("post")->where("user_id",$this->id)->build();
$result = ServiceContainer::Database()->select($query);
return $result[0]["count"];
}
public function isBanned($cat_id) {
$where = new QueryBuilder();
$where = $where->where('user_id',$this->id)->where('category_id',$cat_id)->where('action_type','ban')->where('expired_at','>',date('Y-m-d H:i:s'))->orderBy('expired_at','desc');
$actions = UserAction::select($where);
return (count($actions) > 0);
}
public function isSilenced($cat_id) {
$where = new QueryBuilder();
$where = $where->where('user_id',$this->id)->where('category_id',$cat_id)->where('action_type','silence')->where('expired_at','>',date('Y-m-d H:i:s'))->orderBy('expired_at','desc');
$actions = UserAction::select($where);
return (count($actions) > 0);
}
public function isPardoned($thread_id) {
$where = new QueryBuilder();
$where = $where->where('user_id',$this->id)->where('thread_id',$thread_id)->where('action_type','silence')->where('expired_at','>',date('Y-m-d H:i:s'))->orderBy('expired_at','desc');
$actions = UserAction::select($where);
return (count($actions) > 0);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class UserAction extends DBModel {
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class UserChange extends DBModel {
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class UserFavorite extends DBModel {
}

View File

@ -0,0 +1,8 @@
<?php
namespace Application\Models;
use Application\Foundations\Model as DBModel;
class UserReport extends DBModel {
}

View File

@ -1,6 +1,8 @@
<?php
namespace Application\Services;
use Application\Models\User;
class Authentication {
public function __construct() {
ServiceContainer::Session();
@ -8,4 +10,21 @@ class Authentication {
public function isLoggedIn() {
return ServiceContainer::Session()->has('user_id');
}
public function isModerator() {
if(!$this->isLoggedIn()) return false;
$id = ServiceContainer::Session()->get('user_id');
$user = User::find($id);
return ($user->is_moderator);
}
public function isAdmin() {
if(!$this->isLoggedIn()) return false;
$id = ServiceContainer::Session()->get('user_id');
$user = User::find($id);
return ($user->is_admin);
}
public function user() {
$id = ServiceContainer::Session()->get('user_id');
$user = User::find($id);
return $user;
}
}

View File

@ -32,7 +32,7 @@ class Database {
if($result) {
return mysqli_fetch_all($result,MYSQLI_ASSOC);
} else {
return null;
return mysqli_error($this->conn);
}
}

View File

@ -6,9 +6,15 @@ class View {
ob_start();
extract($args);
$auth = ServiceContainer::Authentication();
$session = ServiceContainer::Session();
$view = $this;
$root = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'];
include('Application/Views/'.$path.'.php');
$rendered_string = ob_get_contents();
ob_end_clean();
return $rendered_string;
}
public function include($path, $args = []) {
echo $this->render($path, $args);
}
}

View File

@ -660,34 +660,6 @@ main {
min-height: calc(100vh - 8rem);
}
@media (min-width: 640px) {
main {
padding-left: 2rem;
padding-right: 2rem;
}
}
@media (min-width: 768px) {
main {
padding-left: 4rem;
padding-right: 4rem;
}
}
@media (min-width: 1024px) {
main {
padding-left: 8rem;
padding-right: 8rem;
}
}
@media (min-width: 1280px) {
main {
padding-left: 16rem;
padding-right: 16rem;
}
}
.header-link {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -734,6 +706,173 @@ h1 {
font-size: 2.25rem;
}
#forumbrowser {
display: -webkit-box;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
}
.forumbrowser-group {
height: 3rem;
text-align: right;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
color: #3b90c6;
cursor: pointer;
}
.forumbrowser-group.selected {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-group:hover {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-category {
height: 3rem;
text-align: right;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
color: #3b90c6;
cursor: pointer;
}
.forumbrowser-category.selected {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-category:hover {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-thread-table {
display: table;
}
.forumbrowser-thread {
text-align: left;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: table-row;
height: 3rem;
color: #3b90c6;
cursor: pointer;
}
.forumbrowser-thread.selected {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-thread:hover {
color: #fff;
background-color: #3b90c6;
}
.forumbrowser-thread-col {
padding-left: 0.5rem;
padding-right: 0.5rem;
display: table-cell;
}
.forum-post {
border-width: 1px;
margin-top: 1rem;
margin-bottom: 1rem;
border-color: #3b90c6;
}
.forum-post-title {
height: 3rem;
width: 100%;
color: #fff;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: -webkit-box;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
-webkit-box-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
align-items: center;
display: flex;
flex-direction: row;
background-color: #3b90c6;
}
.forum-post-content {
display: -webkit-box;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
min-height: 200px;
}
.forum-post-user {
width: 16.666667%;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
}
.forum-post-text {
width: 83.333333%;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.forum-post-footer {
height: 3rem;
width: 100%;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: -webkit-box;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
-webkit-box-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
align-items: center;
}
.forum-post-favorite {
-webkit-box-flex: 1;
flex-grow: 1;
}
.forum-post-actions {
}
.container {
width: 100%;
}

4
Application/Static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

11944
Application/Static/js/vue.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

13
Application/Views/404.php Normal file
View File

@ -0,0 +1,13 @@
<?php
$view->include('layouts/head');
?>
<div class="text-6xl">
<p>Fuee...</p>
</div>
<div>
<p>The page you're trying to find cannot be found</p>
</div>
<?php
$view->include('layouts/foot');
?>

View File

@ -0,0 +1,36 @@
<?php
$view->include('layouts/head');
?>
<div id="login">
<form action="/login/forget" method="POST" id="login">
<h1 class="form-title">Help, I forgot my password!</h1>
<div class="input-group">
<input type="text" name="email" id="email" placeholder="email" v-model="email"></input>
</div>
<div class="input-group">
<p>{{ forget_message }}</p>
</div>
<div class="input-group">
<button type="submit" id="submit" @click="validate">Sign in</button>
</div>
</form>
</div>
<script>
var app = new Vue({
el: "#login",
data: {
email: "",
password: "",
forget_message: "<?php echo $session->get('forget_message') ?? "" ?>",
},
methods: {
validate: function(e) {
this.forget_message = "";
return true;
}
}
});
</script>
<?php
$view->include('layouts/foot');
?>

View File

@ -1,5 +1,5 @@
<?php
include 'layouts/head.php';
$view->include('layouts/head');
?>
<div id="login">
<form action="/login" method="POST" id="login">
@ -10,6 +10,9 @@ include 'layouts/head.php';
<div class="input-group">
<input type="password" name="password" id="password" placeholder="password" v-model="password"></input>
</div>
<div class="input-group">
<a href="/login/forget">I forgot my password</a>
</div>
<div id="errors" v-if="errors != ''">
<p>{{ errors }}</p>
</div>
@ -32,15 +35,6 @@ var app = new Vue({
var emailre = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var emailvalid = emailre.test(String(this.email).toLowerCase()) ;
if(this.username == "") {
this.errors = "Username must not be empty";
e.preventDefault();
return false;
} else if(this.password == "") {
this.errors = "Password must not be empty";
e.preventDefault();
return false;
}
this.errors = "";
return true;
}
@ -48,5 +42,5 @@ var app = new Vue({
});
</script>
<?php
include 'layouts/foot.php';
$view->include('layouts/foot');
?>

View File

@ -0,0 +1,49 @@
<?php
$view->include('layouts/head');
?>
<div id="signup">
<form action="/login/reset" method="POST" id="signup">
<h1 class="form-title">Time to reset your password</h1>
<input type="hidden" name="confirm_key" value="<?php echo $key; ?>">
<div class="input-group">
<input type="password" name="password" id="password" placeholder="password" v-model="password"></input>
</div>
<div class="input-group">
<input type="password" name="confirmpassword" id="confirmpassword" placeholder="confirm password" v-model="confirmpassword"></input>
</div>
<div id="errors" v-if="errors != ''">
<p>{{ errors }}</p>
</div>
<div class="input-group">
<button type="submit" id="submit" @click="validate">Sign Up</button>
</div>
</form>
</div>
<script>
var app = new Vue({
el: "#signup",
data: {
password: "",
confirmpassword: "",
errors: "<?php echo $_SESSION['errors'] ?? "" ?>",
},
methods: {
validate: function(e) {
if(this.password.length < 8) {
this.errors = "Password must be at least 8 characters";
e.preventDefault();
return false;
} else if(this.password !== this.confirmpassword) {
this.errors = "Password and confirm password must be the same";
e.preventDefault();
return false;
}
this.errors = "";
return true;
}
}
});
</script>
<?php
$view->include('layouts/foot');
?>

View File

@ -1,5 +1,5 @@
<?php
include 'layouts/head.php';
$view->include('layouts/head');
?>
<div id="signup">
<form action="/signup" method="POST" id="signup">
@ -72,5 +72,5 @@ var app = new Vue({
});
</script>
<?php
include 'layouts/foot.php';
$view->include('layouts/foot');
?>

View File

@ -0,0 +1,85 @@
<?php if (!$auth->isLoggedIn()) { ?>
<p class="">You need to be logged in to post. <a href="/login">Login</a></p>
<?php } else if($auth->user()->isBanned($category)) {?>
<p class="">You have been banned from this category.</p>
<?php } else if($auth->user()->isSilenced($category)) {?>
<p class="">You have been silenced from this category. You cannot post new replies or threads at this moment.</p>
<?php } else if($auth->user()->isPardoned($thread)) {?>
<p class="">You have been pardoned from this thread. You cannot post new replies at this moment.</p>
<?php } else if(isset($edit_post) && (time() - strtotime($edit_post->created_at)) > 300) {?>
<p class="">You cannot edit this post.</p>
<?php } else { ?>
<div id="editor-comp">
<div class="forum-post" id="forum-editor">
<div class="forum-post-title">
<p class="text-lg flex-grow">
<?php echo $title ?>
</p>
</div>
<div class="forum-post-content">
<div class="forum-post-user">
<a href="/profile?id=<?php echo $auth->user()->id ?>">
<div class="flex flex-col justify-center items-center">
<img src="/noava.jpg">
<p><?php echo $auth->user()->username ?></p>
</div>
</a>
<div class="flex flex-col justify-center items-center">
<p><?php echo $auth->user()->logged_in ? 'Online' : 'Offline' ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $auth->user()->role_string ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $auth->user()->post_count ?> posts</p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $auth->user()->last_login ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<?php if($auth->user()->isBanned($category)) { ?>
<p>Banned</p>
<?php } else if($auth->user()->isSilenced($category)) { ?>
<p>Silenced</p>
<?php } else {?>
<p>Active</p>
<?php } ?>
</div>
</div>
<div class="forum-post-text w-full h-full">
<form method="POST" action="/thread/process">
<input type="hidden" name="category" value="<?php echo $category ?>">
<input type="hidden" name="thread" value="<?php echo $thread ?>">
<input type="hidden" name="reply" value="<?php echo $reply ?>">
<input type="hidden" name="edit" value="<?php echo $edit ?>">
<textarea id="editor-text" class="w-full h-full" name="content"></textarea>
</form>
</div>
</div>
<div class="forum-post-footer">
<div class="forum-post-favorite">
<a class="cursor-pointer" @click="cancel()">Cancel</a>
</div>
<div class="forum-post-actions">
<a class="cursor-pointer" @click="post()">Post</a>
</div>
</div>
</div>
</div>
<script>
var editorapp = new Vue({
el: "#editor-comp",
methods: {
cancel() {
confirm("Are you sure?");
if(confirm) {
$("#editor").html("");
}
},
post() {
}
}
});
</script>
<?php } ?>

View File

@ -1,9 +1,103 @@
<?php
include 'layouts/head.php';
$view->include('layouts/head');
?>
<div id="forumbrowser">
<div class="flex flex-col w-1/6">
<div :id="'group-'+group.id":class="'forumbrowser-group'+(current_group == group.id ? ' selected' : '')" v-for="(group, key) in groups" @click="current_group = group.id;">
{{ group.group_name }}
</div>
</div>
<div class="flex flex-col w-1/6">
<div :id="'category-'+category.id":class="'forumbrowser-category'+(current_category == category.id ? ' selected' : '')" v-for="(category, key) in categories" @click="current_category = category.id;">
{{ category.category_name }}
</div>
</div>
<div class="forumbrowser-thread-table w-4/6">
<div v-if="current_category > 0" id="thread-create" class="forumbrowser-thread" @click="new_thread(current_category)">
<div class="forumbrowser-thread-col"></div>
<div class="forumbrowser-thread-col">Create New Thread</div>
<div class="forumbrowser-thread-col"></div>
<div class="forumbrowser-thread-col"></div>
<div class="forumbrowser-thread-col"></div>
</div>
<div :id="'thread-'+thread.id":class="'forumbrowser-thread'+(current_thread == thread.id ? ' selected' : '')" v-for="(thread, key) in threads" @click="current_thread = thread.id;">
<div class="forumbrowser-thread-col">
<p v-if="thread.is_hot">[HOT]</p>
</div>
<div class="forumbrowser-thread-col">
{{ thread.title }}
</div>
<div class="forumbrowser-thread-col">
by {{ thread.author_model.username }}
</div>
<div class="forumbrowser-thread-col">
View: {{ thread.view_count }} Post count: {{ thread.post_count }}
</div>
<div class="forumbrowser-thread-col">
{{ thread.last_reply }}
</div>
</div>
</div>
</div>
<div id="threadreader">
</div>
<div id="editor">
</div>
<script>
var selectapp = new Vue({
el: "#forumbrowser",
data: {
groups: <?php echo json_encode($groups); ?>,
current_group: 0,
current_category: 0,
current_thread: 0,
current_group: <?php echo isset($group) ? $group->id : 0; ?>,
current_category: <?php echo isset($category) ? $category->id : 0;?>,
current_thread: <?php echo isset($thread) ? $thread->id : 0; ?>,
categories: [],
threads: [],
},
methods: {
change_category(id) {
$.ajax("<?php echo $root; ?>/api/get_threads?id="+id)
.done(function(data) {
this.threads = data;
}.bind(this));
},
change_group(id) {
$.ajax("<?php echo $root; ?>/api/get_categories?id="+id)
.done(function(data) {
this.categories = data;
}.bind(this));
},
change_thread(id) {
$.ajax("<?php echo $root; ?>/thread?id="+id)
.done(function(data) {
$("#threadreader").html(data);
$("#editor").html("");
}.bind(this));
},
new_thread(id) {
$.ajax("<?php echo $root; ?>/thread/editor?category="+id)
.done(function(data) {
$("#threadreader").html("");
$("#editor").html(data);
}.bind(this));
},
},
updated: function() {
if(this.current_thread > 0) {
this.change_thread(this.current_thread);
}
if(this.current_category > 0) {
this.change_category(this.current_category);
}
if(this.current_group > 0) {
this.change_group(this.current_group);
}
},
});
</script>
<?php
include 'layouts/foot.php';
$view->include('layouts/foot');
?>

View File

@ -1,7 +1,8 @@
<html>
<head>
<link href="/css/metaforums.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="/js/vue.js"></script>
<script src="/js/jquery.min.js"></script>
<title>Metaforums</title>
</head>
<body>
@ -12,6 +13,9 @@
<div class="header-middle">
</div>
<div class="header-left">
<?php if($auth->isModerator()) { ?>
<a href="/moderation" class="header-link">User Management</a>
<?php } ?>
<?php if($auth->isLoggedIn()) { ?>
<a href="/logout" class="header-link">Logout</a>
<?php } else {?>

View File

@ -1,9 +0,0 @@
<?php
include 'layouts/head.php';
?>
<h1>You are registered!</h1>
<p>However, you will need to confirm your email address before you can start exploring the wonderful world of our forum</p>
<p>Please check your inbox for further instructions</p>
<?php
include 'layouts/foot.php';
?>

View File

@ -0,0 +1,85 @@
<div id="forum">
<h1 class="text-2xl">Thread in: <?php echo $thread->category()->category_name ?></h1>
<p class="text-4xl"><?php echo $thread->title ?></p>
<p>Posted on <?php echo $thread->created_at ?> by <?php echo $thread->author_model->username ?></p>
<p><?php echo $thread->elapsed_created ?></p>
<div id="forum-posts">
<?php foreach($thread->posts() as $post) { ?>
<div class="forum-post" id="forum-post-<?php echo $post->id ?>">
<div class="forum-post-title">
<p class="text-lg flex-grow"><?php echo $post->title ?></p>
<p class="text-lg"><?php echo $post->elapsed_created; ?></p>
</div>
<div class="forum-post-content">
<div class="forum-post-user">
<a href="/profile?id=<?php echo $post->user()->id ?>">
<div class="flex flex-col justify-center items-center">
<img src="/noava.jpg">
<p><?php echo $post->user()->username ?></p>
</div>
</a>
<div class="flex flex-col justify-center items-center">
<p><?php echo $post->user()->logged_in ? 'Online' : 'Offline' ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $post->user()->role_string ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $post->user()->post_count ?> posts</p>
</div>
<div class="flex flex-col justify-center items-start">
<p><?php echo $post->user()->last_login ?></p>
</div>
<div class="flex flex-col justify-center items-start">
<?php if($post->user()->isBanned($thread->category()->id)) { ?>
<p>Banned</p>
<?php } else if($post->user()->isSilenced($thread->category()->id)) { ?>
<p>Silenced</p>
<?php } else {?>
<p>Active</p>
<?php } ?>
</div>
</div>
<div class="forum-post-text">
<?php echo $post->post ?>
</div>
</div>
<div class="forum-post-footer">
<div class="forum-post-favorite">
0 favorites
</div>
<div class="forum-post-actions">
<?php if(!$thread->lock_moderator && $auth->isLoggedIn()) { ?>
<?php if($post->user_id == $auth->user()->id) { ?>
<a class="cursor-pointer" @click="reply(<?php echo $post->id ?>)">Reply</a>
<a class="cursor-pointer" @click="edit(<?php echo $post->id ?>)">Edit</a>
<a class="cursor-pointer" @click="delete_post(<?php echo $post->id ?>)">Delete</a>
<?php } else { ?>
<a class="cursor-pointer" @click="favorite(<?php echo $post->id ?>)">Favorite</a>
<a class="cursor-pointer" @click="reply(<?php echo $post->id ?>)">Reply</a>
<a class="cursor-pointer" @click="report(<?php echo $post->id ?>)">Report Abuse</a>
<?php } ?>
<?php } ?>
</div>
</div>
</div>
<?php } ?>
</div>
</div>
<script>
var threadapp = new Vue({
el: "#forum",
methods: {
reply(post_id) {
$.ajax("<?php echo $root ?>/thread/editor?thread=<?php echo $thread->id ?>&reply="+post_id).done(function(data) {
$("#editor").html(data);
});
},
edit(post_id) {
$.ajax("<?php echo $root ?>/thread/editor?thread=<?php echo $thread->id ?>&edit="+post_id).done(function(data) {
$("#editor").html(data);
});
},
},
});
</script>

View File

@ -2,6 +2,8 @@
require 'autoload.php';
date_default_timezone_set('Asia/Jakarta');
// Use helper classes from Application
use Application\HTTP\Request;
use Application\HTTP\Response;
@ -39,9 +41,8 @@ $request_method = $_SERVER['REQUEST_METHOD'];
// Get current route from uri
if(!array_key_exists($request_method.':'.$uri,$routes)) {
http_response_code(404);
header('Content-Type: application/json');
die(json_encode([ 'success' => false, 'error' => 'Not found' ]));
$response->statusCode(404)->view('404')->render();
die();
}
$route = $routes[$request_method.':'.$uri];

View File

@ -9,9 +9,6 @@ return [
'POST:/signup' => [
'controller' => 'AuthController@create_user',
],
'GET:/signup/success' => [
'controller' => 'AuthController@sign_up_success',
],
'GET:/signup/confirm' => [
'controller' => 'AuthController@sign_up_confirm',
],
@ -24,4 +21,28 @@ return [
'GET:/logout' => [
'controller' => 'AuthController@logout',
],
'GET:/login/forget' => [
'controller' => 'AuthController@forget_password',
],
'POST:/login/forget' => [
'controller' => 'AuthController@forget_password_confirm',
],
'GET:/login/reset' => [
'controller' => 'AuthController@reset_password',
],
'POST:/login/reset' => [
'controller' => 'AuthController@reset_password_confirm',
],
'GET:/api/get_categories' => [
'controller' => 'ApiController@categories',
],
'GET:/api/get_threads' => [
'controller' => 'ApiController@threads',
],
'GET:/thread' => [
'controller' => 'ForumThreadController@forum',
],
'GET:/thread/editor' => [
'controller' => 'ForumThreadController@editor',
],
];