diff options
Diffstat (limited to 'src/web')
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'; + +?> |