diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..1ec8739
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,6 @@
+
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ /index.php/$1 [L]
+
diff --git a/Application/Controllers/AuthController.php b/Application/Controllers/AuthController.php
new file mode 100644
index 0000000..2394f41
--- /dev/null
+++ b/Application/Controllers/AuthController.php
@@ -0,0 +1,133 @@
+isLoggedIn()) {
+ return $response->redirect('/');
+ }
+ return $response->view('sign-up');
+ }
+ public function create_user(Request $request, Response $response) {
+ if($request->email == "") {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Email must not be empty' ]
+ );
+ } else if (!filter_var($request->email,FILTER_VALIDATE_EMAIL)) {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Email must not valid' ]
+ );
+ } else if ($request->username == "") {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Username must not be empty' ]
+ );
+ } else if (strlen($request->username) < 6 || strlen($request->username) > 20 ) {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Username must be between 6 and 20 characters' ]
+ );
+ } else if (strlen($request->password) < 8) {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Password must be at least 8 characters' ]
+ );
+ } else if ($request->password != $request->confirmpassword) {
+ return $response->redirect("/signup")->with(
+ [ 'errors' => 'Password and confirm password must be the same' ]
+ );
+ }
+ ServiceContainer::Session()->unset('errors');
+ $data = User::create([
+ 'id' => null,
+ 'username' => $request->username,
+ 'about' => '',
+ 'email' => $request->email,
+ 'email_visible' => 0,
+ 'avatar_path' => '',
+ 'password' => password_hash($request->password,PASSWORD_DEFAULT),
+ 'is_confirmed' => 0,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ]);
+ if($data != null) {
+ $id = $data->id;
+ $confirmator = UserConfirmation::create([
+ 'confirm_key' => hash('sha256',$data->username.time()),
+ 'user_id' => $id,
+ 'best_before' => date('Y-m-d H:i:s', strtotime('+6 hours', time())),
+ ]);
+ $email = new MailBuilder();
+ $body = "Thank you for registering with metaforums.\n";
+ $body .= "To be able to explore the vast forum, use the URL below:\n\n";
+ $body .= $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'].'/signup/confirm?'.$confirmator->confirm_key;
+ $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(
+ [ '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)) {
+ $id = $confirm->user_id;
+ $user = User::find($id);
+ $user->update([ 'is_confirmed' => 1 ]);
+ $confirm->delete();
+ }
+ return $response->redirect('/');
+ }
+ public function login(Request $request, Response $response) {
+ if(ServiceContainer::Authentication()->isLoggedIn()) {
+ return $response->redirect('/');
+ }
+ return $response->view('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) {
+ return $response->redirect("/login")->with(
+ [ 'errors' => 'Wrong username or password' ]
+ );
+ } else {
+ $password = $result[0]["password"];
+ $verify = password_verify($request->password,$password);
+ if(!$verify) {
+ return $response->redirect("/login")->with(
+ [ 'errors' => 'Wrong username or password' ]
+ );
+ }
+ }
+ ServiceContainer::Session()->set('user_id',$result[0]['id']);
+ return $response->redirect('/');
+ }
+ public function logout(Request $request, Response $response) {
+ ServiceContainer::Session()->destroy();
+ return $response->redirect('/login');
+ }
+}
diff --git a/Application/Controllers/IndexController.php b/Application/Controllers/IndexController.php
new file mode 100644
index 0000000..a96d863
--- /dev/null
+++ b/Application/Controllers/IndexController.php
@@ -0,0 +1,15 @@
+view('index');
+ }
+}
diff --git a/Application/Foundations/MailBuilder.php b/Application/Foundations/MailBuilder.php
new file mode 100644
index 0000000..7cf285a
--- /dev/null
+++ b/Application/Foundations/MailBuilder.php
@@ -0,0 +1,26 @@
+to = $to;
+ return $this;
+ }
+ public function subject($subject) {
+ $this->subject = $subject;
+ return $this;
+ }
+ public function body($body) {
+ $this->message = $body;
+ return $this;
+ }
+ public function from($from) {
+ $this->headers .= "From: ".$from."\r\n";
+ return $this;
+ }
+}
diff --git a/Application/Foundations/Model.php b/Application/Foundations/Model.php
new file mode 100644
index 0000000..155677b
--- /dev/null
+++ b/Application/Foundations/Model.php
@@ -0,0 +1,71 @@
+attributes = $data;
+ }
+ public static function create($data) {
+ $calling_class = get_called_class();
+ $class = explode('\\',get_called_class());
+ $tablename = strtolower($class[count($class)-1]);
+ $result = ServiceContainer::Database()->insert($tablename, $data);
+ if($result) {
+ $data['id'] = $result;
+ }
+ $inst = new $calling_class();
+ $inst->hydrate($result);
+ return $inst;
+ }
+ public static function find($key) {
+ $calling_class = get_called_class();
+ $inst = new $calling_class();
+ $class = explode('\\',get_called_class());
+ $tablename = strtolower($class[count($class)-1]);
+ $query = new QueryBuilder();
+ $query = $query->select('*')->from($tablename)->where($inst->primary_key,$key)->build();
+ $result = ServiceContainer::Database()->select($query);
+ if(count($result) == 0) return null;
+ $inst->hydrate($result[0]);
+ return $inst;
+ }
+ public function update($key) {
+ $calling_class = get_called_class();
+ $class = explode('\\',get_called_class());
+ $tablename = strtolower($class[count($class)-1]);
+ $query = new QueryBuilder();
+ $query = $query->update($tablename)->set($key)->where($this->primary_key,$this->attributes[$this->primary_key])->build();
+ $result = ServiceContainer::Database()->update($query);
+ if(!$result) return null;
+ else {
+ return $this;
+ }
+ }
+ public function delete() {
+ $calling_class = get_called_class();
+ $class = explode('\\',get_called_class());
+ $tablename = strtolower($class[count($class)-1]);
+ $query = new QueryBuilder();
+ $query = $query->delete()->from($tablename)->where($this->primary_key,$this->attributes[$this->primary_key])->build();
+ $result = ServiceContainer::Database()->update($query);
+ if(!$result) return $this;
+ else {
+ return null;
+ }
+ }
+ function __get($prop) {
+ return $this->attributes[$prop];
+ }
+
+ function __set($prop, $val) {
+ $this->attributes[$prop] = $val;
+ }
+
+}
diff --git a/Application/Foundations/QueryBuilder.php b/Application/Foundations/QueryBuilder.php
new file mode 100644
index 0000000..3ac5add
--- /dev/null
+++ b/Application/Foundations/QueryBuilder.php
@@ -0,0 +1,82 @@
+query .= "SELECT ".$fields;
+ } else {
+ $this->query .= "SELECT ".implode(",",SQLHelper::encode_list($fields));
+ }
+ return $this;
+ }
+ public function delete() {
+ $this->query .= "DELETE";
+ return $this;
+ }
+ public function update($table) {
+ // TODO: SQL injection
+ $this->query .= "UPDATE ".$table;
+ return $this;
+ }
+ public function set($data) {
+ $this->query .= " SET";
+ $final = [];
+ foreach($data as $key => $value) {
+ $final[] = $key." = ".SQLHelper::encode_literal($value);
+ }
+ $this->query .= " ".implode(",",$final);
+ return $this;
+ }
+ public function from($table) {
+ // TODO: SQL injection
+ $this->query .= " FROM ".$table;
+ return $this;
+ }
+ public function where($a, $b, $c = null) {
+ $field = "";
+ $value = "";
+ $operator = "=";
+ if($c == null) {
+ // 2 param syntax
+ $field = $a;
+ $value = $b;
+ } else {
+ $field = $a;
+ $value = $c;
+ $operator = $b;
+ }
+ $value = SQLHelper::encode_literal($value);
+ if($this->where == "") {
+ $this->where .= " WHERE ".$field." ".$operator." ".$value;
+ } else {
+ $this->where .= " AND ".$field." ".$operator." ".$value;
+ }
+ return $this;
+ }
+ public function orWhere($a, $b, $c = null) {
+ $field = "";
+ $value = "";
+ $operator = "=";
+ if($c == null) {
+ // 2 param syntax
+ $field = $a;
+ $value = $b;
+ } else {
+ $field = $a;
+ $value = $c;
+ $operator = $b;
+ }
+ if($this->where == "") {
+ $this->where .= " WHERE ".$field." ".$operator." ".$value;
+ } else {
+ $this->where .= " OR ".$field." ".$operator." ".$value;
+ }
+ return $this;
+ }
+ public function build() {
+ return $this->query.$this->where;
+ }
+}
diff --git a/Application/Foundations/SQLHelper.php b/Application/Foundations/SQLHelper.php
new file mode 100644
index 0000000..e4dea08
--- /dev/null
+++ b/Application/Foundations/SQLHelper.php
@@ -0,0 +1,29 @@
+ $val) {
+ $insert_data[$index] = SQLHelper::encode_literal($val);
+ }
+ return $insert_data;
+ }
+ public static function encode_literal($val) {
+ $db = ServiceContainer::Database();
+ if(is_numeric($val)) {
+ return $val;
+ } else if(is_null($val)) {
+ return 'NULL';
+ } else if(!is_numeric($val)) {
+ return '"'.$db->escapeString($val).'"';
+ } else if($val == "") {
+ return '""';
+ } else {
+ return $val;
+ }
+ }
+}
diff --git a/Application/HTTP/Request.php b/Application/HTTP/Request.php
new file mode 100644
index 0000000..194fa21
--- /dev/null
+++ b/Application/HTTP/Request.php
@@ -0,0 +1,20 @@
+data = $_REQUEST;
+ $this->query = $_SERVER['QUERY_STRING'];
+ }
+ function queryString() {
+ return $this->query;
+ }
+ function __get($prop) {
+ return $this->data[$prop];
+ }
+ function __set($prop, $val) {
+ $this->data[$prop] = $val;
+ }
+}
diff --git a/Application/HTTP/Response.php b/Application/HTTP/Response.php
new file mode 100644
index 0000000..af83924
--- /dev/null
+++ b/Application/HTTP/Response.php
@@ -0,0 +1,48 @@
+body .= ServiceContainer::View()->render($path,$args);
+ return $this;
+ }
+ public function data($data) {
+ return $this->json()->body(json_encode($data));
+ }
+ public function body($body) {
+ $this->body = $body;
+ return $this;
+ }
+ public function statusCode($status) {
+ $this->status = $status;
+ return $this;
+ }
+ public function header($head) {
+ $this->headers[] = $head;
+ return $this;
+ }
+ public function json() {
+ return $this->header('Content-Type: application/json');
+ }
+ public function render() {
+ http_response_code($this->status);
+ foreach($this->headers as $header) {
+ header($header);
+ }
+ echo $this->body;
+ }
+ public function redirect($path) {
+ return $this->header('Location: '.$path);
+ }
+ public function with($data) {
+ foreach($data as $key => $val) {
+ ServiceContainer::Session()->set($key,$val);
+ }
+ return $this;
+ }
+}
diff --git a/Application/Models/User.php b/Application/Models/User.php
new file mode 100644
index 0000000..4d61e2f
--- /dev/null
+++ b/Application/Models/User.php
@@ -0,0 +1,8 @@
+has('user_id');
+ }
+}
diff --git a/backend/Mitsumine/Services/Config.php b/Application/Services/Config.php
similarity index 67%
rename from backend/Mitsumine/Services/Config.php
rename to Application/Services/Config.php
index 675b739..1269313 100644
--- a/backend/Mitsumine/Services/Config.php
+++ b/Application/Services/Config.php
@@ -1,10 +1,10 @@
configs = require 'backend/config.php';
+ $this->configs = require 'config.php';
}
public function __call($name, $args) {
return $this->configs[$name];
diff --git a/Application/Services/Database.php b/Application/Services/Database.php
new file mode 100644
index 0000000..544e294
--- /dev/null
+++ b/Application/Services/Database.php
@@ -0,0 +1,43 @@
+config = ServiceContainer::Config();
+ $this->conn = mysqli_connect($this->config->db_host(),$this->config->db_user(),$this->config->db_pass(),$this->config->db_name());
+ }
+ public function insert($table, $data) {
+ $insert_data = SQLHelper::encode_list($data);
+ $key_names = array_keys($insert_data);
+ $query = "INSERT INTO ".$table." (".implode(",",$key_names).") VALUES (".implode(",",$insert_data).")";
+ $result = mysqli_query($this->conn,$query);
+ if($result) {
+ return mysqli_insert_id($this->conn);
+ } else {
+ echo mysqli_error($this->conn);
+ return null;
+ }
+ }
+ public function update($query) {
+ $result = mysqli_query($this->conn,$query);
+ return $result;
+ }
+ public function select($query) {
+ $result = mysqli_query($this->conn,$query);
+ if($result) {
+ return mysqli_fetch_all($result,MYSQLI_ASSOC);
+ } else {
+ return null;
+ }
+ }
+
+ // Escaping strings requires DB connection, which is only handled by the Database service.
+ public function escapeString($str) {
+ return mysqli_real_escape_string($this->conn,$str);
+ }
+}
diff --git a/Application/Services/Email.php b/Application/Services/Email.php
new file mode 100644
index 0000000..5b79ef6
--- /dev/null
+++ b/Application/Services/Email.php
@@ -0,0 +1,10 @@
+to,$email->subject,$email->message,$email->headers);
+ }
+}
diff --git a/backend/Mitsumine/Services/ServiceContainer.php b/Application/Services/ServiceContainer.php
similarity index 85%
rename from backend/Mitsumine/Services/ServiceContainer.php
rename to Application/Services/ServiceContainer.php
index f43e917..765c680 100644
--- a/backend/Mitsumine/Services/ServiceContainer.php
+++ b/Application/Services/ServiceContainer.php
@@ -1,5 +1,5 @@
+
+
+
+
diff --git a/Application/Views/layouts/foot.php b/Application/Views/layouts/foot.php
new file mode 100644
index 0000000..fb3af42
--- /dev/null
+++ b/Application/Views/layouts/foot.php
@@ -0,0 +1,5 @@
+
+
+