summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rw-r--r--src/web/_controller/_index.php23
-rw-r--r--src/web/_controller/_util/post.php198
-rw-r--r--src/web/_controller/apps/auth.php56
-rw-r--r--src/web/_controller/apps/error.php21
-rw-r--r--src/web/_controller/apps/home.php26
-rw-r--r--src/web/_controller/modal.php34
-rw-r--r--src/web/_controller/template.php22
-rw-r--r--src/web/_model/apps/auth.php13
-rw-r--r--src/web/_model/apps/error.php31
-rw-r--r--src/web/_model/apps/home.php22
-rw-r--r--src/web/_model/cache.php37
-rw-r--r--src/web/_model/format.php45
-rw-r--r--src/web/_model/main.php96
-rw-r--r--src/web/_model/request.php40
-rw-r--r--src/web/_views/apps/auth/login.php86
-rw-r--r--src/web/_views/apps/error/main.php6
-rw-r--r--src/web/_views/apps/home/main.php27
-rw-r--r--src/web/_views/footer.php8
-rw-r--r--src/web/_views/header.php62
-rw-r--r--src/web/_views/header_empty.php23
-rw-r--r--src/web/_views/modal/new_post.php59
-rw-r--r--src/web/_views/modal/register.php173
-rw-r--r--src/web/_views/template/comment.php15
-rw-r--r--src/web/_views/template/error.php12
-rw-r--r--src/web/_views/template/modal.php14
-rw-r--r--src/web/_views/template/pfp.php8
-rw-r--r--src/web/_views/template/post.php86
-rw-r--r--src/web/_views/template/posts.php23
-rw-r--r--src/web/_views/template/toast.php26
-rw-r--r--src/web/config/aesthetic.php64
-rw-r--r--src/web/config/routes.php8
-rw-r--r--src/web/core/_controller.php64
-rw-r--r--src/web/core/_model.php44
-rw-r--r--src/web/core/database.php189
-rw-r--r--src/web/core/loader.php101
-rw-r--r--src/web/core/router.php147
-rw-r--r--src/web/helper/error.php9
-rw-r--r--src/web/helper/lang.php77
-rw-r--r--src/web/index.php33
-rw-r--r--src/web/lang/en_US/api_lang.php32
-rw-r--r--src/web/lang/en_US/apps/auth.php34
-rw-r--r--src/web/lang/en_US/apps/home.php9
-rw-r--r--src/web/lang/en_US/common_lang.php50
-rw-r--r--src/web/lang/en_US/error_lang.php8
44 files changed, 2161 insertions, 0 deletions
diff --git a/src/web/_controller/_index.php b/src/web/_controller/_index.php
new file mode 100644
index 0000000..2fd7db2
--- /dev/null
+++ b/src/web/_controller/_index.php
@@ -0,0 +1,23 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class _index_controller extends Controller {
+
+ // the home model
+ private $home_model;
+
+ // the request model
+ private $request_model;
+
+ // the caceh model
+ private $cache_model;
+
+ public function index(): void {
+ if ($this->main->session) {
+ $this->redirect('/home');
+ } else {
+ $this->redirect('/auth/login');
+ }
+ }
+
+}
+
+?>
diff --git a/src/web/_controller/_util/post.php b/src/web/_controller/_util/post.php
new file mode 100644
index 0000000..b48816d
--- /dev/null
+++ b/src/web/_controller/_util/post.php
@@ -0,0 +1,198 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Post_controller extends Controller {
+
+ // the request model
+ private $request_model;
+
+ // the caceh model
+ private $cache_model;
+
+ // page size
+ private $page_size;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->request_model = $this->load->model('request');
+ $this->cache_model = $this->load->model('cache');
+ $this->page_size = 10;
+ }
+
+ public function index(): void {
+ $this->view('template/posts');
+ }
+
+ public function post(): void {
+ $pid = $this->request_model->get_int('id', 0);
+
+ $post = $this->db
+ ->select('p.*, l.id as like_id')
+ ->from('api.post p')
+ ->join('api.like l', 'p.id = l.post_id AND l.user_id')
+ ->eq($pid)
+ ->where('p.id')
+ ->eq($pid)
+ ->row();
+
+ if (!$post) {
+ return;
+ }
+
+ $users = $this->cache_model->get_users([$post]);
+ $uid = $post['user_id'];
+
+ if (!array_key_exists($uid, $users)) {
+ return;
+ }
+
+ $user = $users[$uid];
+
+ $data = array(
+ 'user' => $user,
+ 'page_size' => $this->page_size,
+ 'post' => $post
+ );
+ $this->view('template/post', $data);
+ }
+
+ /**
+ * @return array<string,mixed>
+ */
+ public function posts(): array {
+ $page = $this->request_model->get_int('page', 0);
+ $max = $this->request_model->get_int('max');
+ $offset = $page * $this->page_size;
+
+ $user = $this->main->user();
+ $uid = isset($user) ? $user['id'] : NULL;
+
+ $query = $this->db;
+
+ $query = $this->db
+ ->select('p.*, l.id as like_id')
+ ->from('api.post p')
+ ->join('api.like l', 'p.id = l.post_id AND l.user_id')
+ ->eq($uid);
+
+ if ($max) {
+ $query = $query
+ ->where('p.id')->le($max);
+ }
+
+ $posts = $query
+ ->order_by('p.id', 'DESC')
+ ->limit($this->page_size)
+ ->offset($offset)
+ ->rows();
+
+ $users = $this->cache_model->get_users($posts);
+ $max = 0;
+
+ foreach ($posts as $post) {
+ $max = max($max, $post['id']);
+ $data = array();
+ $data['page_size'] = $this->page_size;
+ $data['user'] = $users[$post['user_id']];
+ $data['post'] = $post;
+ $this->view('template/post', $data);
+ }
+
+ $pc = $this->db
+ ->select('COUNT(p.id) as pc')
+ ->from('api.post p')
+ ->row()['pc'];
+
+ return array(
+ 'loaded' => count($posts),
+ 'total' => $pc,
+ 'page_size' => $this->page_size,
+ 'max' => $max,
+ );
+ }
+
+ public function comment(): void {
+ $cid = $this->request_model->get_int('id', 0);
+
+ $comment = $this->db
+ ->select('*')
+ ->from('api.comment')
+ ->where('id')
+ ->eq($cid)
+ ->row();
+
+ if (!$comment) {
+ return;
+ }
+
+ $users = $this->cache_model->get_users([$comment]);
+ $uid = $comment['user_id'];
+
+ if (!array_key_exists($uid, $users)) {
+ return;
+ }
+
+ $user = $users[$uid];
+
+ $data = array(
+ 'user' => $user,
+ 'comment' => $comment
+ );
+ $this->view('template/comment', $data);
+ }
+
+ /**
+ * @return array<string,mixed>
+ */
+ public function comments(): array {
+ $page = $this->request_model->get_int('page', 0);
+ $max = $this->request_model->get_int('max');
+ $id = $this->request_model->get_int('id', 0);
+ $offset = $page * $this->page_size;
+
+ $query = $this->db
+ ->select('*')
+ ->from('api.comment')
+ ->where('post_id')
+ ->eq($id);
+
+ if ($max) {
+ $query = $query
+ ->and()
+ ->where('id')
+ ->le($max);
+ }
+
+ $comments = $query
+ ->order_by('id', 'ASC')
+ ->limit($this->page_size)
+ ->offset($offset)
+ ->rows();
+
+ $users = $this->cache_model->get_users($comments);
+ $max = 0;
+
+ // only add this hr when not logged in
+ // otherwise its added automatically by
+ // the like and comment buttons
+ if (
+ count($comments) &&
+ $page == 0 &&
+ $this->main->session === NULL
+ ) {
+ echo '<hr>';
+ }
+
+ foreach ($comments as $comment) {
+ $max = max($max, $comment['id']);
+ $data = array();
+ $data['user'] = $users[$comment['user_id']];
+ $data['comment'] = $comment;
+ $this->view('template/comment', $data);
+ }
+
+ return array(
+ 'loaded' => count($comments),
+ 'page_size' => $this->page_size,
+ 'max' => $max,
+ );
+ }
+}
diff --git a/src/web/_controller/apps/auth.php b/src/web/_controller/apps/auth.php
new file mode 100644
index 0000000..6b30cc9
--- /dev/null
+++ b/src/web/_controller/apps/auth.php
@@ -0,0 +1,56 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Auth_controller extends Controller {
+
+ // the home model
+ private $auth_model;
+
+ // the post controller
+ protected $post_controller;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->auth_model = $this->load->model('apps/auth');
+ }
+
+ public function index(): void {
+ if ($this->main->session) {
+ $this->redirect('/home');
+ } else {
+ $this->redirect('/auth/login');
+ }
+ }
+
+ public function login(): void {
+ if ($this->main->session) {
+ $this->redirect('/home');
+ }
+
+ parent::index();
+ $data = $this->auth_model->get_data();
+ $this->view('header_empty', $data);
+ $this->view('apps/auth/login', $data);
+ $this->view('footer', $data);
+ }
+
+ public function logout(): void {
+ if ($this->main->session) {
+ $_SESSION['jwt'] = NULL;
+ }
+ $this->redirect('/auth/login');
+ }
+
+ public function update(): void {
+ if (!$this->is_ajax()) {
+ $this->error(400);
+ }
+ if (!isset($_POST['key']) || !isset($_POST['value'])) {
+ $this->error(400);
+ }
+ $key = $_POST['key'];
+ $value = $_POST['value'];
+ $_SESSION[$key] = $value;
+ }
+
+}
+
+?>
diff --git a/src/web/_controller/apps/error.php b/src/web/_controller/apps/error.php
new file mode 100644
index 0000000..03bbd8d
--- /dev/null
+++ b/src/web/_controller/apps/error.php
@@ -0,0 +1,21 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Error_controller extends Controller {
+
+ private $error_model;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->error_model = $this->load->model('apps/error');
+ }
+
+ public function index(): void {
+ parent::index();
+ $data = $this->error_model->get_data();
+ $this->view('header', $data);
+ $this->view('apps/error/main', $data);
+ $this->view('footer', $data);
+ }
+
+}
+
+?>
diff --git a/src/web/_controller/apps/home.php b/src/web/_controller/apps/home.php
new file mode 100644
index 0000000..c9a116d
--- /dev/null
+++ b/src/web/_controller/apps/home.php
@@ -0,0 +1,26 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Home_controller extends Controller {
+
+ // the home model
+ private $home_model;
+
+ // the post controller
+ protected $post_controller;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->home_model = $this->load->model('apps/home');
+ $this->post_controller = $this->load->controller('_util/post');
+ }
+
+ public function index(): void {
+ parent::index();
+ $data = $this->home_model->get_data();
+ $this->view('header', $data);
+ $this->view('apps/home/main', $data);
+ $this->view('footer', $data);
+ }
+
+}
+
+?>
diff --git a/src/web/_controller/modal.php b/src/web/_controller/modal.php
new file mode 100644
index 0000000..03074d4
--- /dev/null
+++ b/src/web/_controller/modal.php
@@ -0,0 +1,34 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Modal_controller extends Controller {
+
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ /**
+ * @param string $name
+ * @param array $data
+ */
+ private function modal($name, $data = array()): void {
+ $title = lang($name . '_modal_title');
+ $data['title'] = $title;
+ $data['content'] = $name;
+ $this->view('template/modal', $data);
+ }
+
+ public function new_post(): void {
+ $this->modal('new_post');
+ }
+
+ public function register(): void {
+ $this->load->app_lang(
+ $this->main->info['lang'],
+ 'auth'
+ );
+ $this->modal('register');
+ }
+}
+
+?>
+
diff --git a/src/web/_controller/template.php b/src/web/_controller/template.php
new file mode 100644
index 0000000..7a8cdf8
--- /dev/null
+++ b/src/web/_controller/template.php
@@ -0,0 +1,22 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Template_controller extends Controller {
+
+ // the request model
+ private $request_model;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->request_model = $this->load->model('request');
+ }
+
+ public function toast(): void {
+ $data = array(
+ 'msg' => $this->request_model->get_str('msg', FALSE),
+ 'detail' => $this->request_model->get_str('detail', FALSE),
+ 'hint' => $this->request_model->get_str('hint', FALSE)
+ );
+ $this->view('template/toast', $data);
+ }
+
+}
+
diff --git a/src/web/_model/apps/auth.php b/src/web/_model/apps/auth.php
new file mode 100644
index 0000000..a1802de
--- /dev/null
+++ b/src/web/_model/apps/auth.php
@@ -0,0 +1,13 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Auth_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ public function get_data(): array {
+ $data = parent::get_data();
+ $data['title'] = lang('login');
+ return $data;
+ }
+}
diff --git a/src/web/_model/apps/error.php b/src/web/_model/apps/error.php
new file mode 100644
index 0000000..58e3346
--- /dev/null
+++ b/src/web/_model/apps/error.php
@@ -0,0 +1,31 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Error_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ private function get_msg(&$data) {
+ if (!array_key_exists('code', $_GET)) {
+ http_response_code(500);
+ $data['msg'] = lang('error');
+ $data['title'] = '500';
+ } else {
+ $code = $_GET['code'];
+ http_response_code($code);
+ $data['title'] = $code;
+ $msg = lang('error_' . $code, FALSE);
+ if (!$msg) {
+ $msg = lang('error');
+ }
+ $data['msg'] = $msg;
+ }
+ }
+
+ public function get_data(): array {
+ $data = parent::get_data();
+ $this->get_msg($data);
+ return $data;
+ }
+}
+?>
diff --git a/src/web/_model/apps/home.php b/src/web/_model/apps/home.php
new file mode 100644
index 0000000..82fbf26
--- /dev/null
+++ b/src/web/_model/apps/home.php
@@ -0,0 +1,22 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Home_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ private function get_posts(): array {
+ return $this->db
+ ->select('*')
+ ->from('admin.post')
+ ->limit(20)
+ ->rows();
+ }
+
+ public function get_data(): array {
+ $data = parent::get_data();
+ $data['title'] = lang('title');
+ $data['posts'] = $this->get_posts();
+ return $data;
+ }
+}
diff --git a/src/web/_model/cache.php b/src/web/_model/cache.php
new file mode 100644
index 0000000..6cf9924
--- /dev/null
+++ b/src/web/_model/cache.php
@@ -0,0 +1,37 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Cache_model extends Model {
+
+ // the user cache
+ private $users;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->users = array();
+ }
+
+ /**
+ * Gets a array of users
+ */
+ public function get_users($objs) {
+ $ids = array();
+ foreach ($objs as $obj) {
+ $id = $obj['user_id'];
+ if (!array_key_exists($id, $this->users)) {
+ array_push($ids, intval($id));
+ }
+ }
+ if (!empty($ids)) {
+ $result = $this->main->db
+ ->select('*')
+ ->from('api.user')
+ ->where_in('id', $ids)
+ ->rows();
+ foreach ($result as $user) {
+ $id = $user['id'];
+ $this->users[$id] = $user;
+ }
+ }
+ return $this->users;
+ }
+
+}
diff --git a/src/web/_model/format.php b/src/web/_model/format.php
new file mode 100644
index 0000000..52b51be
--- /dev/null
+++ b/src/web/_model/format.php
@@ -0,0 +1,45 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Format_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ /**
+ * Formats a users's name
+ * @param array $user - the $user
+ * @returns the user's formatted display name
+ */
+ public function name($user) {
+ $name = '';
+ if ($user['first_name']) {
+ $name .= $user['first_name'];
+ }
+ if ($user['middle_name']) {
+ if ($name != '') {
+ $name .= ' ';
+ }
+ $name .= $user['middle_name'];
+ }
+ if ($user['last_name']) {
+ if ($name != '') {
+ $name .= ' ';
+ }
+ $name .= $user['last_name'];
+ }
+ if ($name == '') {
+ $name = '@' . $user['username'];
+ }
+ return $name;
+ }
+
+ /**
+ * Formats a date
+ * @param string $date - the data in RFC3999 format
+ * @returns the formatted date
+ */
+ public function date($date) {
+ return $date;
+ }
+
+}
diff --git a/src/web/_model/main.php b/src/web/_model/main.php
new file mode 100644
index 0000000..6d8b708
--- /dev/null
+++ b/src/web/_model/main.php
@@ -0,0 +1,96 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Main_model {
+
+ // the website database
+ public $db;
+
+ // the current user session (can be NULL)
+ public $session;
+
+ // current loaded users
+ private $users;
+
+ // stores the current request info
+ public $info;
+
+ /**
+ * Loads the main model
+ * @param Loader $load - the main loader object
+ */
+ function __construct($load) {
+ /// load the database helper
+ $this->db = new DatabaseHelper();
+ /// load the current session
+ if (array_key_exists('jwt', $_SESSION)) {
+ $this->get_session($_SESSION['jwt']);
+ } else {
+ $this->session = NULL;
+ };
+ /// init other vars
+ $this->users = array();
+ }
+
+ /**
+ * Loads current session
+ * @param string $jwt - the user provided JWT
+ */
+ private function get_session($jwt) {
+ $query = $this->db
+ ->select("_api.verify_jwt('" . $jwt . "') AS user_id;");
+ $result = $query->row();
+ $user_id = $result['user_id'];
+ if ($user_id) {
+ $this->session = array(
+ 'id' => $user_id,
+ 'jwt' => $jwt
+ );
+ }
+ }
+
+ /**
+ * Gets the stamp for a asset path
+ * @param string $path
+ */
+ private function asset_stamp($path): int {
+ $root = $GLOBALS['webroot'];
+ $path = $root . '/../public/' . $path;
+ return filemtime($path);
+ }
+
+ /**
+ * Loads a css html link
+ * @param string $path - the path to the css file
+ */
+ public function link_css($path) {
+ $stamp = $this->asset_stamp($path);
+ return '<link rel="stylesheet" href="/public/' . $path . '?stamp=' . $stamp . '">';
+ }
+
+ /**
+ * Loads a js html link
+ * @param string $path - the path to the js file
+ */
+ public function link_js($path) {
+ $stamp = $this->asset_stamp($path);
+ return '<script src="/public/'. $path . '?stamp=' . $stamp . '"></script>';
+ }
+
+ /**
+ * Gets the current user
+ */
+ public function user() {
+ if ($this->session) {
+ return $this->db
+ ->select('*')
+ ->from('api.user')
+ ->where('id')
+ ->eq($this->session['id'])
+ ->row();
+ } else {
+ return NULL;
+ }
+ }
+
+}
+
+?>
diff --git a/src/web/_model/request.php b/src/web/_model/request.php
new file mode 100644
index 0000000..4cce07a
--- /dev/null
+++ b/src/web/_model/request.php
@@ -0,0 +1,40 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Request_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ /**
+ * Loads a string from the GET request
+ * @param string $key - the name for the query param
+ * @param string $default - the default value if not exists
+ */
+ public function get_str($key, $default = NULL): string | NULL {
+ if (!array_key_exists($key, $_GET)) {
+ return $default;
+ } else {
+ return $_GET[$key];
+ }
+ }
+
+ /**
+ * Loads a number from the GET request
+ * @param string $key - the name for the query param
+ * @param int $default - the default value if not exists
+ */
+ public function get_int($key, $default = NULL): int | NULL {
+ if (!array_key_exists($key, $_GET)) {
+ return $default;
+ } else {
+ $val = $_GET[$key];
+ $val = intval($val);
+ if ($val < 0) {
+ return 0;
+ } else {
+ return $val;
+ }
+ }
+ }
+
+}
diff --git a/src/web/_views/apps/auth/login.php b/src/web/_views/apps/auth/login.php
new file mode 100644
index 0000000..d7f326b
--- /dev/null
+++ b/src/web/_views/apps/auth/login.php
@@ -0,0 +1,86 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div id="main-content">
+ <div class="branding col">
+ <h1>xssbook</h1>
+ <span><?=lang('login_branding')?></span>
+ </div>
+ <div class="form card col">
+ <form id="action-login" class="col" action="">
+ <div class="rel mb">
+ <input
+ type="text"
+ name="username"
+ id="login-username"
+ placeholder=" "
+ >
+ <label for="username">
+ <?=lang('ph_username')?>
+ </label>
+ </div>
+ <div class="rel mb">
+ <input
+ type="password"
+ name="password"
+ id="login-password"
+ placeholder=" "
+ >
+ <label for="password">
+ <?=lang('ph_password')?>
+ </label>
+ </div>
+ <?=ilang('action_login',
+ class: 'btn btn-submit btn-wide',
+ button: TRUE,
+ attrs: array('type' => 'submit')
+ )?>
+ <?=ilang('action_forgot_passwd',
+ class: 'btn btn-line btn-blue btn-wide mt'
+ )?>
+ </form>
+ <hr>
+ <?=ilang('action_create_account',
+ id: 'action-register',
+ class: 'btn btn-success btn-wide',
+ button: TRUE,
+ attrs: array('type' => 'submit')
+ )?>
+ </div>
+ <script>
+
+ var onLogin = function(data) {
+ let jwt = data.token;
+
+ $.ajax({
+ url: '/auth/update',
+ method: 'POST',
+ data: JSON.stringify({
+ key: 'jwt',
+ value: jwt
+ }),
+ success: function (_) {
+ window.location = '/home';
+ }
+ })
+ };
+
+ $('#action-login').on('submit', function(e) {
+ e.preventDefault();
+ let username = $('#login-username').val();
+ let password = $('#login-password').val();
+
+ $.ajax({
+ url: '/api/rpc/login',
+ method: 'POST',
+ data: JSON.stringify({ username, password }),
+ success: onLogin
+ });
+ });
+
+ $('#action-register').on('click', function() {
+ $.get( "/modal/register", function (data) {
+ $(document.body).append(data);
+ });
+ })
+ </script>
+</div>
diff --git a/src/web/_views/apps/error/main.php b/src/web/_views/apps/error/main.php
new file mode 100644
index 0000000..dde39cf
--- /dev/null
+++ b/src/web/_views/apps/error/main.php
@@ -0,0 +1,6 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div id="main-content">
+ <h1><?=$title?></h1>
+ <span><?=$msg?></span>
+</div>
diff --git a/src/web/_views/apps/home/main.php b/src/web/_views/apps/home/main.php
new file mode 100644
index 0000000..29bf7c3
--- /dev/null
+++ b/src/web/_views/apps/home/main.php
@@ -0,0 +1,27 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div id="main-content">
+<?php if ($self): ?>
+ <div id="new-post" class="card">
+ <div class="row grow">
+ <?php $this->view('template/pfp', array('user' => $self))?>
+ <a
+ id="action-new-post"
+ class="btn btn-alt btn-wide ml"
+ autocomplete="off"
+ aria-label="<?=lang('action_new_post_tip')?>"
+ >
+ <?=lang('action_new_post_text', sub: [$self['first_name']])?>
+ </a>
+ </div>
+ <script>
+ $('#action-new-post').on('click', function() {
+ $.get( "/modal/new_post", function (data) {
+ $(document.body).append(data);
+ });
+ })
+ </script>
+ </div>
+<?php endif; ?>
+ <?php $this->post_controller->index(); ?>
+</div>
diff --git a/src/web/_views/footer.php b/src/web/_views/footer.php
new file mode 100644
index 0000000..9040c3a
--- /dev/null
+++ b/src/web/_views/footer.php
@@ -0,0 +1,8 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+ <footer>
+ Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
+ </footer>
+ <body>
+
+</html>
diff --git a/src/web/_views/header.php b/src/web/_views/header.php
new file mode 100644
index 0000000..7c60197
--- /dev/null
+++ b/src/web/_views/header.php
@@ -0,0 +1,62 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<?php
+ $self = $this->main->user();
+ $this->view('header_empty', $data);
+?>
+ <header class="nav">
+ <div class="nav-left">
+ <span class="logo">xssbook</span>
+ </div>
+ <div class="nav-center" :class="{hidden: !visible}">
+ <a
+ id="action-home"
+ class="btn"
+ href="/home"
+ title="<?=lang('action_home_tip')?>"
+ >
+ <i class="mi mi-lg">home</i>
+ <span><?=lang('action_home_text')?></span>
+ </a>
+ <a
+ id="action-people"
+ class="btn"
+ href="/people"
+ title="<?=lang('action_people_tip')?>"
+ >
+ <i class="mi mi-lg">people</i>
+ <span><?=lang('action_people_text')?></span>
+ </a>
+ <a
+ id="action-chat"
+ class="btn"
+ href="/chat"
+ title="<?=lang('action_chat_tip')?>"
+ >
+ <i class="mi mi-lg">chat</i>
+ <span><?=lang('action_chat_text')?></span>
+ </a>
+ </div>
+ <div class="nav-right">
+ <button
+ id="action-hamburger"
+ title="<?=lang('action_hamburger_tip')?>"
+ >
+ <i class="mi mi-lg">menu</i>
+ </button>
+ <?php if($self): ?>
+ <?php $this->view('template/pfp', array(
+ 'user' => $self,
+ 'class' => 'pfp-sm ml',
+ )); ?>
+ <?php else: ?>
+ <?=ilang('action_login', class: 'btn', href: '/auth/login')?>
+ <?php endif; ?>
+ </div>
+ <script>
+ $('#action-hamburger').on('click', function() {
+ let menu = $('.nav-center');
+ menu.toggleClass('visible');
+ });
+ </script>
+ </header>
diff --git a/src/web/_views/header_empty.php b/src/web/_views/header_empty.php
new file mode 100644
index 0000000..75f6f17
--- /dev/null
+++ b/src/web/_views/header_empty.php
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ <?php if ($this->main->session): ?>
+ var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
+ <?php else: ?>
+ var jwtStr = null;
+ <?php endif; ?>
+ </script>
+ <?php
+ foreach ($js_files as $js) {
+ echo $this->main->link_js($js);
+ }
+ foreach ($css_files as $css) {
+ echo $this->main->link_css($css);
+ }
+ ?>
+ <title><?=$title?></title>
+ </head>
+ <body>
+ <div id="toast-container">
+ </div>
diff --git a/src/web/_views/modal/new_post.php b/src/web/_views/modal/new_post.php
new file mode 100644
index 0000000..50b9b84
--- /dev/null
+++ b/src/web/_views/modal/new_post.php
@@ -0,0 +1,59 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<?php
+ $user = $this->main->user();
+?>
+<form id="new-post-form">
+<div class="modal-content new-post-modal">
+ <div class="row">
+ <?php $this->view('template/pfp', array('user' => $user))?>
+ <div class="col ml">
+ <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
+ <span class="dim"><?=lang('now')?></span>
+ </div>
+ </div>
+ <textarea
+ type="text"
+ name="content"
+ id="new-post-content"
+ placeholder="<?=lang('action_new_post_text', sub: [$user['first_name']])?>"
+ ></textarea>
+</div>
+<div class="modal-footer">
+ <?=ilang('action_submit',
+ id: 'new-post-submit',
+ class: 'btn btn-wide btn-submit',
+ attrs: array('type' => 'submit'),
+ button: TRUE
+ )?>
+</div>
+</form>
+<script>
+ $('#new-post-form').submit(function(e) {
+ e.preventDefault();
+ let content = $('#new-post-content').val();
+ let me = $(this);
+
+ const getPost = function(data) {
+ if (data) {
+ $('#post-container').prepend(data);
+ }
+ me.closest('.modal-container').remove();
+ }
+
+ const onPost = function(data) {
+ let id = data[0].id;
+ $.get({
+ url: '/_util/post/post?id=' + id,
+ success: getPost
+ });
+ }
+
+ $.ajax({
+ url: '/api/post',
+ method: 'POST',
+ data: JSON.stringify({ content }),
+ success: onPost
+ });
+ });
+</script>
diff --git a/src/web/_views/modal/register.php b/src/web/_views/modal/register.php
new file mode 100644
index 0000000..f4d364a
--- /dev/null
+++ b/src/web/_views/modal/register.php
@@ -0,0 +1,173 @@
+
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<form id="register-form">
+<div class="modal-content register-modal col">
+ <label class="static">
+ <?=lang('ph_basic_info')?>
+ </label>
+ <div class="row mt">
+ <div class="rel btn-wide">
+ <input
+ type="text"
+ name="first_name"
+ id="register-first-name"
+ placeholder=" "
+ >
+ <label for="first_name">
+ <?=lang('ph_first_name')?>
+ </label>
+ </div>
+ <div class="rel ml btn-wide">
+ <input
+ type="text"
+ name="last_name"
+ id="register-last-name"
+ placeholder=" "
+ >
+ <label for="last_name">
+ <?=lang('ph_last_name')?>
+ </label>
+ </div>
+ </div>
+ <div class="rel mt">
+ <input
+ type="text"
+ name="username"
+ id="register-username"
+ placeholder=" "
+ >
+ <label for="username">
+ <?=lang('ph_username')?>
+ </label>
+ </div>
+ <div class="rel mt">
+ <input
+ type="password"
+ name="password"
+ id="register-password"
+ placeholder=" "
+ >
+ <label for="password">
+ <?=lang('ph_password')?>
+ </label>
+ </div>
+ <div class="rel mt">
+ <input
+ type="text"
+ name="email"
+ id="register-email"
+ placeholder=" "
+ >
+ <label for="email">
+ <?=lang('ph_email')?>
+ </label>
+ </div>
+ <label for="birth_date" class="mt static">
+ <?=lang('ph_birth_date')?>
+ </label>
+ <input
+ class="mt"
+ type="date"
+ name="birth_date"
+ id="register-birth-date"
+ >
+ <label for="gender" class="mt static">
+ <?=lang('ph_gender')?>
+ </label>
+ <div class="row mt" data-type="radio" data-name="gender-wrapper">
+ <div class="rel radio mr">
+ <input
+ type="radio"
+ id="register-gender-male"
+ name="gender"
+ value="male"
+ >
+ <label
+ for="register-gender-male"
+ class="static"
+ >
+ <?=lang('ph_gender_male')?>
+ </label>
+ </div>
+ <div class="rel radio mr">
+ <input
+ type="radio"
+ id="register-gender-female"
+ name="gender"
+ value="female"
+ >
+ <label
+ for="register-gender-female"
+ class="static"
+ >
+ <?=lang('ph_gender_female')?>
+ </label>
+ </div>
+ <div class="rel radio">
+ <input
+ type="radio"
+ id="register-gender-lettuce"
+ name="gender"
+ value="lettuce"
+ >
+ <label
+ for="register-gender-lettuce"
+ class="static"
+ >
+ <?=lang('ph_gender_lettuce')?>
+ </label>
+ </div>
+ </div>
+</div>
+<div class="modal-footer">
+ <?=ilang('action_register',
+ id: 'register-submit',
+ class: 'btn btn-wide btn-success',
+ attrs: array('type' => 'submit'),
+ button: TRUE
+ )?>
+</div>
+</form>
+<script>
+ $('#register-form').submit(function(e) {
+ e.preventDefault();
+
+ const form = event.target;
+ const formFields = form.elements;
+
+ let first_name = formFields.first_name.value.trim();
+ let last_name = formFields.last_name.value.trim();
+ let username = formFields.username.value.trim();
+ let password = formFields.password.value.trim();
+ let email = formFields.email.value.trim();
+ let birth_date = formFields.birth_date.value.trim();
+ let gender = formFields.gender.value.trim();
+
+ if(birth_date === '') {
+ errorToast('toast_date_empty');
+ return;
+ }
+
+ const onSuccess = () => {
+ $.ajax({
+ url: '/api/rpc/login',
+ method: 'POST',
+ data: JSON.stringify({
+ username, password
+ }),
+ success: onLogin
+ });
+ };
+
+ $.ajax({
+ url: '/api/user',
+ method: 'POST',
+ data: JSON.stringify({
+ first_name, last_name, username, password,
+ email, birth_date, gender
+ }),
+ success: onSuccess
+ });
+ });
+</script>
diff --git a/src/web/_views/template/comment.php b/src/web/_views/template/comment.php
new file mode 100644
index 0000000..3ff473b
--- /dev/null
+++ b/src/web/_views/template/comment.php
@@ -0,0 +1,15 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<?php
+ $format_model = $this->load->model('format');
+?>
+<div class="comment row mt">
+ <?php $this->view('template/pfp', array('user' => $user))?>
+ <div class="ml col sub-card">
+ <div class="row">
+ <strong><?=$format_model->name($user)?></strong>
+ <span class="dim ml"><?=$format_model->date($comment['created'])?></span>
+ </div>
+ <?=$comment['content']?>
+ </div>
+</div>
diff --git a/src/web/_views/template/error.php b/src/web/_views/template/error.php
new file mode 100644
index 0000000..2e02cb1
--- /dev/null
+++ b/src/web/_views/template/error.php
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><?=$code . ' - ' . $msg?></title>
+ </head>
+ <body>
+ <center>
+ <h1><?=$code . ' ' . $msg?></h1>
+ </center>
+ <hr>
+ </body>
+</html>
diff --git a/src/web/_views/template/modal.php b/src/web/_views/template/modal.php
new file mode 100644
index 0000000..e3ce6fe
--- /dev/null
+++ b/src/web/_views/template/modal.php
@@ -0,0 +1,14 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div class="modal-container">
+ <div class="modal">
+ <div class="modal-header row">
+ <?=$title?>
+ <?=ilang(
+ 'action_modal_close',
+ class: 'float-right btn btn-action modal-close',
+ )?>
+ </div>
+ <?php $this->view('modal/' . $content) ?>
+ </div>
+</div>
diff --git a/src/web/_views/template/pfp.php b/src/web/_views/template/pfp.php
new file mode 100644
index 0000000..aec7318
--- /dev/null
+++ b/src/web/_views/template/pfp.php
@@ -0,0 +1,8 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<?php
+ $class = isset($class) ? $class : '';
+?>
+<a class="image-loading pfp <?=$class?>" href="/profile?id=<?=$user['id']?>">
+ <img src="/api/rpc/avatar?user_id=<?=$user['id']?>" />
+</a>
diff --git a/src/web/_views/template/post.php b/src/web/_views/template/post.php
new file mode 100644
index 0000000..83a72bf
--- /dev/null
+++ b/src/web/_views/template/post.php
@@ -0,0 +1,86 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div class="post card">
+ <div class="row">
+ <?php $this->view('template/pfp', array('user' => $user))?>
+ <div class="col ml">
+ <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
+ <span class="dim"><?=$post['created']?></span>
+ </div>
+ </div>
+ <p>
+ <?=$post['content']?>
+ </p>
+<?php
+ $self = $this->main->user();
+ $liked = $post['like_id'] ? 'btn-blue' : '';
+ $post_attrs = array(
+ 'postId' => $post['id']
+ );
+ if ($post['like_id'] !== NULL) {
+ $post_attrs['likeId'] = $post['like_id'];
+ }
+?>
+<?php if ($self): ?>
+ <hr>
+ <div class="row">
+ <?=ilang('action_like',
+ class: 'btn btn-wide action-like ' . $liked,
+ attrs: $post_attrs
+ )?>
+ <?=ilang('action_comment', class: 'btn btn-wide action-comment',
+ click: '$(\'#action-new-comment-' . $post['id'] . '\').focus()'
+ )?>
+ </div>
+ <hr>
+<?php endif; ?>
+ <div class="col comments pb">
+ <?php
+ $_GET = array('id' => $post['id']);
+ $cdata = $this->comments();
+
+ $loaded = $cdata['loaded'];
+ $max = $cdata['max'];
+ $page_size = $cdata['page_size'];
+ $total = $post['comment_count'];
+
+ if ($loaded >= $page_size && $page_size < $total) {
+ ilang('action_load_comments',
+ class: 'action-load-comments btn btn-line mt',
+ attrs: array(
+ 'postId' => $post['id'],
+ 'loaded' => $loaded,
+ 'pageSize' => $page_size,
+ 'commentCount' => $total,
+ 'commentMax' => $max,
+ )
+ );
+ }
+
+ ?>
+ </div>
+<?php if ($self): ?>
+ <div class="row pb">
+ <?php $this->view('template/pfp', array('user' => $user))?>
+ <form class="ml action-new-comment-form row">
+ <input
+ type="hidden"
+ name="id"
+ value="<?=$post['id']?>"
+ >
+ <input
+ id="action-new-comment-<?=$post['id']?>"
+ class="action-new-comment btn btn-wide btn-alt"
+ postId="<?=$post['id']?>"
+ autocomplete="off"
+ type="text"
+ name="text"
+ placeholder="<?=lang('action_new_comment_text')?>"
+ aria-label="<?=lang('action_new_comment_tip')?>"
+ >
+ </form>
+ </div>
+<?php endif; ?>
+</div>
+
+
diff --git a/src/web/_views/template/posts.php b/src/web/_views/template/posts.php
new file mode 100644
index 0000000..5e9156c
--- /dev/null
+++ b/src/web/_views/template/posts.php
@@ -0,0 +1,23 @@
+<div id="post-container">
+<?php
+ $pdata = $this->posts();
+
+ $loaded = $pdata['loaded'];
+ $page_size = $pdata['page_size'];
+ $total = $pdata['total'];
+ $max = $pdata['max'];
+
+ if ($loaded >= $page_size && $page_size < $total) {
+ ilang('action_load_posts',
+ id: 'action-load-posts',
+ class: 'btn btn-line btn-wide mb',
+ attrs: array(
+ 'loaded' => $loaded,
+ 'pageSize' => $page_size,
+ 'postCount' => $total,
+ 'postMax' => $max,
+ )
+ );
+ }
+?>
+</div>
diff --git a/src/web/_views/template/toast.php b/src/web/_views/template/toast.php
new file mode 100644
index 0000000..ae2e7d8
--- /dev/null
+++ b/src/web/_views/template/toast.php
@@ -0,0 +1,26 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<?php
+ $params = array();
+
+ if ($detail) {
+ array_push($params, lang('api_column_' . $detail));
+ }
+
+ if ($hint) {
+ array_push($params, $hint);
+ }
+
+ $lang_msg = lang($msg, FALSE, sub: $params);
+
+ if(!$lang_msg) {
+ $lang_msg = $msg;
+ } else {
+ $lang_msg = ucfirst($lang_msg);
+ }
+
+?>
+<div class="toast error">
+ <?=$lang_msg?>
+ <?=ilang('action_close', class: 'action-close-toast')?>
+</div>
diff --git a/src/web/config/aesthetic.php b/src/web/config/aesthetic.php
new file mode 100644
index 0000000..304baec
--- /dev/null
+++ b/src/web/config/aesthetic.php
@@ -0,0 +1,64 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Aesthetic {
+
+ private $config;
+
+ function __construct() {
+ $this->config = array(
+ '_common' => array(
+ 'js' => [
+ 'js/thirdparty/jquery.min.js',
+ 'js/lib.js',
+ 'js/modal.js',
+ ],
+ 'css' => [
+ 'css/common.css'
+ ],
+ ),
+ 'error' => array(
+ 'css' => [
+ 'css/error.css'
+ ],
+ ),
+ 'home' => array(
+ 'js' => [
+ 'js/routes/home.js',
+ 'js/post.js',
+ ],
+ 'css' => [
+ 'css/home.css',
+ 'css/post.css'
+ ],
+ ),
+ 'auth' => array(
+ 'css' => [
+ 'css/auth.css'
+ ],
+ ),
+ );
+ }
+ /**
+ * @param mixed $route
+ * @return array<string,>
+ */
+ function get_files($route): array {
+ $js_files = $this->config['_common']['js'];
+ $css_files = $this->config['_common']['css'];
+
+ if (array_key_exists($route, $this->config)) {
+ $config = $this->config[$route];
+ if (array_key_exists('js', $config)) {
+ $js_files = array_merge($js_files, $config['js']);
+ }
+ if (array_key_exists('css', $config)) {
+ $css_files = array_merge($css_files, $config['css']);
+ }
+ }
+
+ return array(
+ 'js_files' => $js_files,
+ 'css_files' => $css_files,
+ );
+ }
+
+}
diff --git a/src/web/config/routes.php b/src/web/config/routes.php
new file mode 100644
index 0000000..33c871b
--- /dev/null
+++ b/src/web/config/routes.php
@@ -0,0 +1,8 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+$routes = array();
+$routes['home'] = 'apps/home';
+$routes['error'] = 'apps/error';
+$routes['auth'] = 'apps/auth';
+
+$routes[''] = '_index';
diff --git a/src/web/core/_controller.php b/src/web/core/_controller.php
new file mode 100644
index 0000000..4a788d3
--- /dev/null
+++ b/src/web/core/_controller.php
@@ -0,0 +1,64 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+abstract class Controller {
+
+ // the main model
+ public $main;
+
+ // the loader
+ public $load;
+
+ // the database
+ public $db;
+
+ /**
+ * Creates a constructor
+ * @param Loader $load - the website loaded object
+ */
+ function __construct($load) {
+ $this->load = $load;
+ $this->main = $this->load->model('main');
+ $this->db = $this->main->db;
+
+ $info = $this->main->info;
+ $lang = $info['lang'];
+ $this->load->lang($lang);
+ $app = $info['app'];
+ if ($app) {
+ $this->load->app_lang($lang, $app);
+ }
+ }
+
+ public function index() {}
+
+ public function redirect($link) {
+ header('Location: '. $link, true, 301);
+ die();
+ }
+
+ protected function view($__name, $data = array()) {
+ $__root = $GLOBALS['webroot'];
+ $__path = $__root . '/_views/' . $__name . '.php';
+ if (is_file($__path)) {
+ extract($data);
+ require($__path);
+ return;
+ }
+ }
+
+ protected function is_ajax(): bool {
+ $_POST = json_decode(
+ file_get_contents("php://input"), true
+ );
+ return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
+ }
+
+ protected function error($code): void {
+ $_GET['code'] = $code;
+ $this->main->info['app'] = 'error';
+ $error_controller = $this->load->controller('apps/error');
+ $error_controller->index();
+ die();
+ }
+
+}
+?>
diff --git a/src/web/core/_model.php b/src/web/core/_model.php
new file mode 100644
index 0000000..936fab4
--- /dev/null
+++ b/src/web/core/_model.php
@@ -0,0 +1,44 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+abstract class Model {
+ // the main model
+ // shared by all controllers and models
+ public $main;
+ public $load;
+
+ // the database
+ public $db;
+
+ private $config;
+
+ /**
+ * Creates a model
+ * @param Loader $load - the main loader object
+ */
+ function __construct($load) {
+ $this->load = $load;
+ $this->main = $this->load->model('main');
+ $this->db = $this->main->db;
+ $this->config = new Aesthetic();
+ }
+
+ /**
+ * @returns the base model data
+ */
+ public function get_data(): array {
+ $data = array();
+ $data['self'] = $this->main->user();
+
+ $info = $this->main->info;
+ $app = $info['app'];
+
+ if ($app) {
+ $files = $this->config->get_files($app);
+ $data = array_merge($data, $files);
+ } else {
+ $files = $this->config->get_files();
+ $data = array_merge($data, $files);
+ }
+
+ return $data;
+ }
+}
diff --git a/src/web/core/database.php b/src/web/core/database.php
new file mode 100644
index 0000000..81352a9
--- /dev/null
+++ b/src/web/core/database.php
@@ -0,0 +1,189 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+class DatabaseQuery {
+
+ private $conn;
+ private $query;
+
+ private $where;
+ private $set;
+
+ private $param;
+
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->query = '';
+
+ $this->set = FALSE;
+ $this->where = FALSE;
+ $this->param = array();
+ }
+
+ private function in($array) {
+ $in = 'IN (';
+ foreach ($array as $idx => $item) {
+ if ($idx != 0) {
+ $in .= ",";
+ }
+ $in .= "?";
+ array_push($this->param, $item);
+ }
+ $in .= ")";
+ return $in;
+ }
+
+ public function select($select) {
+ $this->query .= "SELECT $select\n";
+ return $this;
+ }
+
+ public function from($from) {
+ $this->query .= "FROM $from\n";
+ return $this;
+ }
+
+ public function where($cond) {
+ if (!$this->where) {
+ $this->where = TRUE;
+ $this->query .= "WHERE ";
+ }
+ $this->query .= "$cond ";
+ return $this;
+ }
+
+ public function like($item) {
+ $this->query .= "LIKE ?\n";
+ array_push($this->param, $item);
+ return $this;
+ }
+
+ public function eq($item) {
+ $this->query .= "= ?\n";
+ array_push($this->param, $item);
+ return $this;
+ }
+
+ public function ne($item) {
+ $this->query .= "<> ?\n";
+ array_push($this->param, $item);
+ return $this;
+ }
+
+ public function lt($item) {
+ $this->query .= "< ?\n";
+ array_push($this->param, $item);
+ return $this;
+ }
+
+ public function le($item) {
+ $this->query .= "<= ?\n";
+ array_push($this->param, $item);
+ return $this;
+ }
+
+ public function where_in($column, $array) {
+ if (!$this->where) {
+ $this->where = TRUE;
+ $this->query .= "WHERE ";
+ }
+ if (empty($array)) {
+ $this->query .= "FALSE\n";
+ return $this;
+ }
+ $in = $this->in($array);
+ $this->query .= "$column $in\n";
+ return $this;
+ }
+
+ public function and() {
+ $this->query .= "AND ";
+ return $this;
+ }
+
+ public function or() {
+ $this->query .= "OR ";
+ return $this;
+ }
+
+ public function join($table, $on, $type = 'LEFT') {
+ $this->query .= "$type JOIN $table ON $on\n";
+ return $this;
+ }
+
+ public function limit($limit) {
+ $this->query .= "LIMIT ?\n";
+ array_push($this->param, $limit);
+ return $this;
+ }
+
+ public function offset($offset) {
+ $this->query .= "OFFSET ?\n";
+ array_push($this->param, $offset);
+ return $this;
+ }
+
+ public function order_by($column, $order = 'ASC') {
+ $this->query .= "ORDER BY " . $column . ' ' . $order . ' ';
+ return $this;
+ }
+
+ public function rows() {
+ $stmt = $this->conn->prepare($this->query);
+ try {
+ $stmt->execute($this->param);
+ } catch (Exception $ex) {
+ echo $ex;
+ echo '<br> >> caused by <<<br>';
+ echo str_replace("\n", "<br>", $this->query);
+ }
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ public function row() {
+ $stmt = $this->conn->prepare($this->query);
+ $stmt->execute($this->param);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+}
+
+/**
+ * DatabaseHelper
+ * allows queries on the
+ * xssbook postgres database
+ */
+class DatabaseHelper {
+
+ private $conn;
+
+ function __construct() {
+ $this->conn = NULL;
+ }
+
+ private function connect() {
+ if ($this->conn === NULL) {
+ $user = getenv("POSTGRES_USER");
+ $pass = getenv("POSTGRES_PASSWORD");
+ $db = getenv("POSTGRES_DB");
+ $host = 'db';
+ $port = '5432';
+
+ $conn_str = sprintf("pgsql:host=%s;port=%d;dbname=%s;user=%s;password=%s",
+ $host,
+ $port,
+ $db,
+ $user,
+ $pass
+ );
+ $this->conn = new \PDO($conn_str);
+ $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ }
+ return $this->conn;
+ }
+
+ public function select($select) {
+ $conn = $this->connect();
+ $query = new DatabaseQuery($conn);
+ return $query->select($select);
+ }
+
+}
diff --git a/src/web/core/loader.php b/src/web/core/loader.php
new file mode 100644
index 0000000..2091533
--- /dev/null
+++ b/src/web/core/loader.php
@@ -0,0 +1,101 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Loader {
+
+ // keep track of what has been loaded
+ private $loaded;
+
+ function __construct() {
+ $this->loaded = array();
+ }
+
+ /**
+ * Loads a $type of object from a $dir with a given $name
+ * @param string $name - the name of the object to load
+ * @param string $dir - the directory theese objects are stored in
+ * @param string $type - the type of the object
+ */
+ private function load_type($name, $dir, $type): object|NULL {
+ $path = $dir . '/' . $name . '.php';
+ if (array_key_exists($path, $this->loaded)) {
+ return $this->loaded[$path];
+ }
+
+ if (!file_exists($path)) {
+ return NULL;
+ }
+
+ $parts = explode('/', $name);
+ $part = end($parts);
+ $class = ucfirst($part) . '_' . $type;
+ require($path);
+
+ $ref = NULL;
+ try {
+ $ref = new ReflectionClass($class);
+ } catch (Exception $_e) {}
+
+ if ($ref === NULL) {
+ return NULL;
+ }
+
+ $obj = $ref->newInstance($this);
+ $this->loaded[$path] = $obj;
+
+ return $obj;
+ }
+
+ /**
+ * Loads a model
+ * @param string $name - the name of the model to load
+ */
+ public function model($name): object|NULL {
+ $root = $GLOBALS['webroot'];
+ $dir = $root . '/_model';
+ return $this->load_type($name, $dir, 'model');
+ }
+
+ /**
+ * Loads a controller
+ * @param string $name - the name of the controller to load
+ */
+ public function controller($name): Controller|NULL {
+ $root = $GLOBALS['webroot'];
+ $dir = $root . '/_controller';
+ return $this->load_type($name, $dir, 'controller');
+ }
+
+ /**
+ * Loads the given common lang
+ * @param string $lang_code 0 the language code
+ */
+ public function lang($lang_code): void {
+ $dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/';
+ $lang = $GLOBALS['lang'];
+ if ($handle = opendir($dir)) {
+ while (false !== ($entry = readdir($handle))) {
+ if ($entry === '.' || $entry === '..' || $entry === 'apps') {
+ continue;
+ }
+ $path = $dir . $entry;
+ require($path);
+ }
+ }
+ $GLOBALS['lang'] = $lang;
+ }
+
+ /**
+ * Loads a given app specific lang
+ * @param string $lang_code - the language code
+ * @param string $name - the name of the app
+ */
+ public function app_lang($lang_code, $name): void {
+ $dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/apps/';
+ $file = $dir . $name . '.php';
+ if (file_exists($file)) {
+ $lang = $GLOBALS['lang'];
+ require($dir . $name . '.php');
+ $GLOBALS['lang'] = $lang;
+ }
+ }
+
+}
diff --git a/src/web/core/router.php b/src/web/core/router.php
new file mode 100644
index 0000000..72c7674
--- /dev/null
+++ b/src/web/core/router.php
@@ -0,0 +1,147 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Router {
+
+ // the loader
+ private $load;
+
+ // the main model
+ private $main;
+
+ /**
+ * Creates a router
+ * @param Loader $load - the main laoder object
+ */
+ function __construct($load) {
+ $this->load = $load;
+ $this->main = $this->load->model('main');
+ }
+
+ /**
+ * @param string $path - the current request path
+ * Gets the current route
+ * @return array<string,mixed>
+ */
+ private function get_req_route($path): array {
+ // trim the path
+ $path = trim($path);
+ // remove first '/'
+ $path = substr($path, 1);
+ // get path parts
+ $parts = explode('/', $path);
+
+ $len = count($parts);
+
+ // get route info
+ $route = array();
+ // e.g. /
+ if ($path === '') {
+ $route = array(
+ 'route' => '',
+ 'slug' => 'index',
+ );
+ // e.g. /home /login
+ } else if ($len === 1) {
+ $route = array(
+ 'route' => $parts[0],
+ 'slug' => 'index',
+ );
+ // e.g. /home/posts
+ } else {
+ $route = array (
+ 'route' => implode('/', array_slice($parts, 0, -1)),
+ 'slug' => end($parts)
+ );
+ };
+
+ $route['app'] = $route['route'];
+ $routes = $GLOBALS['routes'];
+ if (array_key_exists($route['route'], $routes)) {
+ $route['route'] = $routes[$route['route']];
+ }
+
+ return $route;
+ }
+
+ /**
+ * Gets the curret request info
+ * @return array<string,mixed>
+ */
+ private function get_req(): array {
+ $method = $_SERVER['REQUEST_METHOD'];
+
+ $uri = parse_url($_SERVER['REQUEST_URI']);
+ $path = $uri['path'];
+
+ return array_merge(
+ array(
+ 'uri' => $uri,
+ 'method' => $method,
+ 'lang' => $this->get_lang(),
+ ),
+ $this->get_req_route($path),
+ );
+ }
+
+ /**
+ * Gets the current language
+ * @return string
+ */
+ private function get_lang(): string {
+ return 'en_US';
+ }
+
+ /**
+ * Handles a router error code
+ * @param int $code - the http error code
+ * @param bool $recursed
+ */
+ private function handle_error($code, $recursed): void {
+ if ($recursed) {
+ die($code . ' (recursed)');
+ }
+
+ $this->main->info['slug'] = 'index';
+ $this->main->info['app'] = 'error';
+ $this->main->info['route'] = 'apps/error';
+ $req = $this->main->info;
+ $_GET['code'] = $code;
+
+ $this->handle_req($req, TRUE);
+ }
+
+ /**
+ * @param array $req
+ * @param bool $recursed
+ */
+ private function handle_req($req, $recursed = FALSE): void {
+ $controller = $this->load->controller($req['route']);
+
+ if ($controller === NULL) {
+ $this->handle_error(404, $recursed);
+ return;
+ }
+
+ $ref = NULL;
+ try {
+ $ref = new ReflectionMethod($controller, $req['slug']);
+ } catch (Exception $_e) {}
+
+ if ($ref === NULL || !$ref->isPublic()) {
+ $this->handle_error(404, $recursed);
+ return;
+
+ }
+
+ $ref->invoke($controller);
+ }
+
+ /**
+ * Handels the incomming reuqest
+ */
+ public function handle_request(): void {
+ $req = $this->get_req();
+ $this->main->info = $req;
+ $this->handle_req($req);
+ }
+
+}
diff --git a/src/web/helper/error.php b/src/web/helper/error.php
new file mode 100644
index 0000000..6fcaddd
--- /dev/null
+++ b/src/web/helper/error.php
@@ -0,0 +1,9 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+function error_page($code, $msg) {
+ $root = $GLOBALS['webroot'];
+ error_reporting(E_ERROR | E_PARSE);
+ http_response_code($code);
+ require($root . '/views/template/error.php');
+ die();
+}
diff --git a/src/web/helper/lang.php b/src/web/helper/lang.php
new file mode 100644
index 0000000..48acba9
--- /dev/null
+++ b/src/web/helper/lang.php
@@ -0,0 +1,77 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+$lang = array();
+
+function lang($key, $default = NULL, $sub = NULL) {
+ $lang = $GLOBALS['lang'];
+ if(array_key_exists($key, $lang)) {
+ if ($sub) {
+ return sprintf($lang[$key], ...$sub);
+ } else {
+ return $lang[$key];
+ }
+ } else if ($default !== NULL) {
+ return $default;
+ } else {
+ return $key;
+ }
+}
+
+function ilang($key,
+ $class = NULL,
+ $id = NULL,
+ $href = NULL,
+ $click = NULL,
+ $attrs = array(),
+ $sub = NULL,
+ $button = FALSE,
+) {
+ $text = lang($key . "_text", FALSE, sub: $sub);
+ $tip = lang($key . "_tip", FALSE);
+ $icon = lang($key . "_icon", FALSE);
+ $content = lang($key . "_content", FALSE);
+
+ if ($click || $button) {
+ echo '<button ';
+ } else {
+ echo '<a ';
+ }
+ if ($tip) {
+ echo 'title="' . $tip . '" ';
+ echo 'aria-label="' . $tip . '" ';
+ }
+ if ($class) {
+ echo 'class="' . $class . '" ';
+ }
+ if ($id) {
+ echo 'id="' . $id . '" ';
+ }
+ if ($click) {
+ echo 'onclick="' . $click . '" ';
+ }
+ if ($href) {
+ echo 'href="' . $href . '" ';
+ }
+ foreach ($attrs as $key => $attr) {
+ echo $key . '="' . $attr . '" ';
+ }
+ echo '> ';
+ if ($icon) {
+ echo '<i class="' . $icon . '">';
+ if ($content) {
+ echo $content;
+ }
+ echo '</i>';
+ }
+ if ($text) {
+ echo '<span';
+ if ($icon) {
+ echo ' class="ml-sm"';
+ }
+ echo '>' . $text . '</span>';
+ }
+ if ($click || $button) {
+ echo '</button>';
+ } else {
+ echo '</a>';
+ }
+}
diff --git a/src/web/index.php b/src/web/index.php
new file mode 100644
index 0000000..688383f
--- /dev/null
+++ b/src/web/index.php
@@ -0,0 +1,33 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+session_save_path('/var/lib/php/session');
+session_start();
+
+$webroot = dirname(__FILE__);
+
+// load all the helper files
+require($webroot . '/helper/error.php');
+require($webroot . '/helper/lang.php');
+
+// load all the config files
+require($webroot . '/config/aesthetic.php');
+require($webroot . '/config/routes.php');
+
+// load all core files
+require($webroot . '/core/_controller.php');
+require($webroot . '/core/_model.php');
+require($webroot . '/core/database.php');
+require($webroot . '/core/loader.php');
+require($webroot . '/core/router.php');
+
+function __init() {
+ $load = new Loader();
+ $router = new Router($load);
+ $router->handle_request();
+};
+
+if (!file_exists('/status/ready')) {
+ error_page(503, 'Service Unavailable');
+}
+
+__init();
diff --git a/src/web/lang/en_US/api_lang.php b/src/web/lang/en_US/api_lang.php
new file mode 100644
index 0000000..3afc4f6
--- /dev/null
+++ b/src/web/lang/en_US/api_lang.php
@@ -0,0 +1,32 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+// user column
+$lang['api_column_username'] = 'username';
+$lang['api_column_first_name'] = 'first name';
+$lang['api_column_last_name'] = 'last name';
+$lang['api_column_middle_name'] = 'middle name';
+$lang['api_column_email'] = 'email';
+$lang['api_column_password'] = 'password';
+$lang['api_column_gender'] = 'gender';
+$lang['api_column_join_date'] = 'join date';
+$lang['api_column_birth_date'] = 'birth date';
+$lang['api_column_profile_avatar'] = 'avatar image';
+$lang['api_column_profile_banner'] = 'banner image';
+$lang['api_column_profile_bio'] = 'profile bio';
+
+// post column
+$lang['api_column_content'] = 'post content';
+
+// error messages
+$lang['api_denied'] = 'Action was denied';
+$lang['api_null_value'] = '%s cannot be empty';
+$lang['api_unique_value'] = '%s is not available (not unique)';
+$lang['api_min_value'] = '%s length cannot be less than %s';
+$lang['api_max_value'] = '%s length cannot exceed %s';
+$lang['api_invalid_login'] = 'Invalid username or password';
+$lang['api_unknown'] = 'An unknown error as occurred';
+
+// toast messages
+$lang['toast_date_empty'] = 'Birthday cannot be empty';
+
+?>
diff --git a/src/web/lang/en_US/apps/auth.php b/src/web/lang/en_US/apps/auth.php
new file mode 100644
index 0000000..fb9d758
--- /dev/null
+++ b/src/web/lang/en_US/apps/auth.php
@@ -0,0 +1,34 @@
+<?php
+
+$lang['login'] = 'Login';
+$lang['login_branding'] = 'Connect with javascript and the world around you on XSSBook.';
+
+$lang['ph_username'] = 'Username';
+$lang['ph_password'] = 'Password';
+$lang['ph_first_name'] = 'First Name';
+$lang['ph_last_name'] = 'Last Name';
+$lang['ph_middle_name'] = 'Middle Name';
+$lang['ph_username'] = 'Username';
+$lang['ph_email'] = 'Email';
+$lang['ph_password'] = 'Password';
+$lang['ph_birth_date'] = 'Birthday';
+$lang['ph_gender'] = 'Gender';
+$lang['ph_gender_male'] = 'Male';
+$lang['ph_gender_female'] = 'Female';
+$lang['ph_gender_lettuce'] = 'Lettuce';
+$lang['ph_basic_info'] = 'General Information';
+
+$lang['action_login_tip'] = 'Login';
+$lang['action_login_text'] = 'Login';
+$lang['action_register_tip'] = 'Register';
+$lang['action_register_text'] = 'Register';
+$lang['action_create_account_tip'] = 'Create a new account';
+$lang['action_create_account_text'] = 'Create new account';
+$lang['action_forgot_passwd_tip'] = 'Reset your password';
+$lang['action_forgot_passwd_text'] = 'Forgot password?';
+
+$lang['register_modal_title'] = 'Create New Account';
+$lang['action_register_text'] = 'Register';
+$lang['action_register_tip'] = 'Register';
+
+?>
diff --git a/src/web/lang/en_US/apps/home.php b/src/web/lang/en_US/apps/home.php
new file mode 100644
index 0000000..a30eb88
--- /dev/null
+++ b/src/web/lang/en_US/apps/home.php
@@ -0,0 +1,9 @@
+<?php
+
+$lang['title'] = 'Home';
+
+// actions
+$lang['action_load_posts_text'] = 'Load more posts';
+$lang['action_load_posts_tip'] = 'Load more posts';
+
+?>
diff --git a/src/web/lang/en_US/common_lang.php b/src/web/lang/en_US/common_lang.php
new file mode 100644
index 0000000..7e214b5
--- /dev/null
+++ b/src/web/lang/en_US/common_lang.php
@@ -0,0 +1,50 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+// Navigation Bar Lang
+$lang['action_home_text'] = 'Home';
+$lang['action_home_tip'] = 'Goto your home page.';
+$lang['action_people_text'] = 'People';
+$lang['action_people_tip'] = 'View xssbook users.';
+$lang['action_chat_text'] = 'Chat';
+$lang['action_chat_tip'] = 'Goto your chat center.';
+$lang['action_profile_tip'] = 'View account options.';
+$lang['action_hamburger_tip'] = 'View header dropdown.';
+$lang['action_login_text'] = 'Login';
+$lang['action_login_tip'] = 'Login or signup';
+
+// Post lang
+$lang['action_like_text'] = 'Like';
+$lang['action_like_tip'] = 'Like this post.';
+$lang['action_like_icon'] = 'mi mi-sm';
+$lang['action_like_content'] = 'thumb_up';
+$lang['action_comment_text'] = 'Comment';
+$lang['action_comment_tip'] = 'Focus the comment box.';
+$lang['action_comment_icon'] = 'mi mi-sm';
+$lang['action_comment_content'] = 'comment';
+$lang['action_new_comment_text'] = 'Write a comment';
+$lang['action_new_comment_tip'] = 'Write a comment, then press enter to submit.';
+$lang['action_load_comments_text'] = 'Load more comments';
+$lang['action_load_comments_tip'] = 'Load more comments';
+
+// General
+$lang['action_submit_text'] = 'Submit';
+$lang['action_submit_tip'] = 'Submit';
+$lang['action_close_text'] = '';
+$lang['action_close_tip'] = 'Close';
+$lang['action_close_icon'] = 'mi mi-sm';
+$lang['action_close_content'] = 'close';
+
+// Modals
+$lang['action_modal_close_text'] = '';
+$lang['action_modal_close_tip'] = 'Close modal.';
+$lang['action_modal_close_icon'] = 'mi mi-sm';
+$lang['action_modal_close_content'] = 'close';
+
+$lang['new_post_modal_title'] = 'Author New Post';
+$lang['action_new_post_text'] = 'What\'s on your mind, %s';
+$lang['action_new_post_tip'] = 'Author a new post.';
+
+// Words
+$lang['now'] = 'Now';
+
+?>
diff --git a/src/web/lang/en_US/error_lang.php b/src/web/lang/en_US/error_lang.php
new file mode 100644
index 0000000..afecaa1
--- /dev/null
+++ b/src/web/lang/en_US/error_lang.php
@@ -0,0 +1,8 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+
+$lang['error_400'] = 'Bad request';
+$lang['error_404'] = 'Resource not found';
+$lang['error_500'] = 'Whoops! Server error :(';
+$lang['error'] = 'An unknown error has occoured';
+
+?>