This commit is contained in:
Freya Murphy 2024-05-20 17:11:38 -04:00
parent 4c8d58b646
commit 708594d32f
Signed by: freya
GPG key ID: 744AB800E383AE52
34 changed files with 854 additions and 51 deletions

10
LICENSE Normal file
View file

@ -0,0 +1,10 @@
The MIT License (MIT)
Copyright © 2023 Freya Murphy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

39
README.md Normal file
View file

@ -0,0 +1,39 @@
## xssbook
### description
who doesn't want to run non free javascript
now with xssbook you can run as much stallman disapprovement as you want
- all inputs on the site are unfiltered
- api calls dont care what you send them as long as they are valid strings
- upload anyfiles to be your profile avatar and banner (even adobe flash!!!)
- /apidocs for api documentation
### installation
XXSBook v2 is a multi docker image setup. To run, download the repoistory, build the docker images, and then start the stack.
```
# download the images
git clone https://g.freya.cat/freya/xssbook2 xssbook2
cd xssbook2
# build and run the stack
docker compose pull
docker compose build
docker compose up -d
```
The only configuration that you many want to change is the http port listed in the docker compose file. By default this is set to port 80, but it can be changed to whatever you want.
### migrating from xssbook v1
If you are runing a xssbook v1 setup, the database is fully incompatible with xssbook v2. Luckily there is a migration that exists to port over your data. XSSBook v1 has a single sqlite database file and a custom assets directory likly called `custom`. You will know you have the right directory if there are two sub directories called `avatar` and `banner`. Place the sqlite db file (called `xssbook.db`) and the `custom` directory in the `data/shim` folder of the xssbook v2 directory. If this doesnt exist please do a full setup of v2 first. Then run `docker compose up -d shim`, and you should be all set.
> WARNING: This will delete ALL data in the database if you specify xssbook v1 files in the data path. Make sure yo only run this once and remove the files once completed.
> NOTE: the migration will never run if the database files are not supplied.
### license
This project is licensed under the MIT license

View file

@ -1,5 +1,5 @@
FROM alpine:3.19
RUN apk add --no-cache postgresql16-client tini
COPY ./dbinit /usr/local/bin/dbinit
COPY ./init /usr/local/bin/init
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/local/bin/dbinit"]
CMD ["/usr/local/bin/init"]

View file

@ -1,7 +1,7 @@
FROM alpine:3.19
COPY ./postgrest.tar.xz /tmp/postgrest.tar.xz
RUN tar xJf /tmp/postgrest.tar.xz -C /tmp
RUN mv /tmp/postgrest /usr/local/bin/postgrest
RUN cp /tmp/postgrest /usr/local/bin/postgrest
RUN rm /tmp/postgrest.tar.xz
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
CMD ["/usr/local/bin/entrypoint.sh"]

9
build/shim/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM alpine:3.19
RUN apk add --no-cache \
php83 \
php83-pdo \
php83-pdo_sqlite \
php83-pdo_pgsql \
tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/bin/php83", "/opt/shim/shim.php"]

View file

@ -38,13 +38,15 @@ services:
rest:
build: ./build/postgrest
restart: unless-stopped
env_file:
- ./conf/postgres/database.env
depends_on:
- db
init:
build: ./build/dbinit
build: ./build/init
restart: no
env_file:
- ./conf/postgres/database.env
volumes:
@ -53,8 +55,21 @@ services:
depends_on:
- db
shim:
build: ./build/shim
restart: no
env_file:
- ./conf/postgres/database.env
volumes:
- ./src/shim:/opt/shim:ro
- ./data/status:/status:ro
- ./data/shim/:/data:ro
depends_on:
- db
swagger:
image: swaggerapi/swagger-ui
restart: unless-stopped
environment:
SWAGGER_JSON_URL: '/api'
BASE_URL: '/apidocs'

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,26 @@
CREATE FUNCTION api.delete_user_media(
media_type admin.user_media_type
)
RETURNS void
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_user_id INTEGER;
_data BYTEA;
BEGIN
_user_id = _api.get_user_id();
DELETE FROM
admin.user_media
WHERE
"type" = media_type AND
"user_id" = _user_id;
END
$BODY$;
GRANT EXECUTE ON FUNCTION api.delete_user_media(admin.user_media_type)
TO rest_user;
GRANT DELETE ON TABLE admin.user_media
TO rest_user;
GRANT UPDATE ON TABLE sys.user_media_id_seq
TO rest_user;

View file

@ -0,0 +1,41 @@
CREATE FUNCTION api.update_user_media(
media_type admin.user_media_type,
mime TEXT,
content TEXT
)
RETURNS void
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_user_id INTEGER;
_data BYTEA;
BEGIN
_user_id = _api.get_user_id();
_data = decode(content, 'base64');
INSERT INTO admin.user_media (
user_id,
content,
mime,
type
) VALUES (
_user_id,
_data,
mime,
media_type
) ON CONFLICT (
"user_id", "type"
) DO UPDATE SET
"content" = excluded.content,
"mime" = excluded.mime,
"type" = excluded.type,
"modified" = clock_timestamp();
END
$BODY$;
GRANT EXECUTE ON FUNCTION api.update_user_media(admin.user_media_type, TEXT, TEXT)
TO rest_user;
GRANT INSERT, UPDATE ON TABLE admin.user_media
TO rest_user;
GRANT UPDATE ON TABLE sys.user_media_id_seq
TO rest_user;

View file

@ -57,6 +57,8 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/media/_api_serve_user_or_default_media.sql;
\i /db/rest/media/api_profile_avatar.sql;
\i /db/rest/media/api_profile_banner.sql;
\i /db/rest/media/api_update_user_media.sql;
\i /db/rest/media/api_delete_user_media.sql;
-- login
\i /db/rest/login/_api_sign_jwt.sql;

View file

@ -23,7 +23,11 @@ CREATE VIEW api.user AS
COALESCE(p.pc, 0)
AS post_count,
COALESCE(l.lc, 0)
AS like_count
AS like_count,
ma.mime
AS avatar_mime,
mb.mime
AS banner_mime
FROM
admin.user u
LEFT JOIN (
@ -81,6 +85,28 @@ CREATE VIEW api.user AS
) l
ON
u.id = l.user_id
LEFT JOIN (
SELECT
ma.mime,
ma.user_id
FROM
admin.user_media ma
WHERE
ma.type = 'avatar'
) ma
ON
u.id = ma.user_id
LEFT JOIN (
SELECT
mb.mime,
mb.user_id
FROM
admin.user_media mb
WHERE
mb.type = 'banner'
) mb
ON
u.id = mb.user_id
WHERE
u.deleted <> TRUE;

View file

@ -310,13 +310,7 @@ input.btn:focus {
display: block;
}
@keyframes shimmer {
to {
background-position-x: 0%;
}
}
.pfp, .pfp img {
.pfp, .pfp .inner {
height: 2.5rem;
border-radius: 2.5rem;
aspect-ratio: 1;
@ -324,15 +318,18 @@ input.btn:focus {
display: block;
}
.pfp-sm, .pfp-sm img {
.pfp-sm, .pfp-sm .inner {
height: 1.75rem;
}
object.inner {
pointer-events: none;
}
.image-loading {
background: linear-gradient(-45deg, var(--surface0) 0%, var(--base) 25%, var(--surface0) 50%);
background-size: 500%;
background-position-x: 150%;
animation: shimmer 1s linear infinite;
}
.image-loaded {

View file

@ -35,7 +35,7 @@
font-size: 1.5rem;
}
.profile .pfp, .profile .pfp img {
.profile .pfp, .profile .pfp .inner {
padding: none;
margin: none;
width: 100%;

View file

@ -8,6 +8,11 @@
justify-content: center;
}
#post-container {
max-width: 40rem;
width: 100%;
}
.post, #new-post {
margin-bottom: 1rem;
max-width: 40rem;

View file

@ -25,6 +25,11 @@
aspect-ratio: 5;
}
.banner object.inner {
width: 100%;
height: 100%;
}
#profile-header .banner img {
height: 100%;
width: 100%;
@ -32,7 +37,7 @@
}
#profile-header .info .pfp-wrapper .pfp,
#profile-header .info .pfp-wrapper .pfp img {
#profile-header .info .pfp-wrapper .pfp .inner {
height: 12.5rem;
}

View file

@ -0,0 +1,13 @@
#main-content {
display: flex;
justify-content: center;
}
#settings {
margin: 0 1rem;
margin-bottom: 1rem;
max-width: 800px;
min-width: 400px;
width: 100%;
}

View file

@ -94,6 +94,13 @@ var errorToast = (msg) => {
})
}
var successToast = (msg) => {
let url = '/template/toast?type=success&msg=' + msg;
$.get(url, function (data) {
$('#toast-container').prepend(data);
})
}
$$('.action-close-toast').on('click', function() {
$(this).parent().remove();
});

285
src/shim/shim.php Executable file
View file

@ -0,0 +1,285 @@
#!/usr/bin/env php
<?php /* Copyright (c) 2024 Freya Murphy */
function wait_until_ready() {
while (TRUE) {
if (file_exists("/status/ready")) {
echo "database ready!\n";
break;
}
echo "waiting for database...\n";
sleep(3);
}
}
if (!file_exists("/data/xssbook.db")) {
echo "/data/xssbook.db not found: exiting shim\n";
die();
}
function connect_psql() {
$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
);
$conn = new \PDO($conn_str);
$conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $conn;
}
function connect_sqlite() {
$conn_str = sprintf("sqlite:/data/xssbook.db");
$conn = new \PDO($conn_str);
$conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $conn;
}
$psql = connect_psql();
$sqlite = connect_sqlite();
function error() {
$psql->rollBack();
die();
}
function get_date(
$day, $month, $year
) {
if (checkdate($month, $day, $year)) {
$date = "{$year}-{$month}-{$day}";
return $date;
} else {
return "1970-01-01";
}
}
function clear_all() {
echo "clearing database\n";
extract($GLOBALS);
$psql->beginTransaction();
$psql->exec(
'DELETE FROM admin.user;
DELETE FROM admin.post;
DELETE FROM admin.comment;
DELETE FROM admin.like;
DELETE FROM admin.follow;
DELETE FROM admin.user_media;'
);
}
function migrate_users() {
echo "migrating users\n";
extract($GLOBALS);
// load queries
$query = $sqlite->prepare(
'SELECT user_id, firstname, lastname, email, password, gender, date, day, month, year FROM users;'
);
$submit = $psql->prepare(
'INSERT INTO admin.user
(id, username, password, first_name, last_name, email, gender, birth_date, created)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, to_timestamp(?));'
);
// load users
$query->execute();
$rows = $query->fetchAll();
foreach ($rows as $user) {
// submit each user
$date = get_date(
$user['day'], $user['month'], $user['year']
);
$joined = $user['date'] / 1000;
$submit->execute(array(
$user['user_id'],
$user['email'],
$user['password'],
$user['firstname'],
$user['lastname'],
$user['email'],
$user['gender'],
$date,
$joined
));
}
}
function migrate_posts() {
echo "migrating posts\n";
extract($GLOBALS);
// load queries
$query = $sqlite->prepare(
'SELECT post_id, user_id, content, date FROM posts;'
);
$submit = $psql->prepare(
'INSERT INTO admin.post
(id, user_id, content, created)
VALUES
(?, ?, ?, to_timestamp(?));'
);
// load posts
$query->execute();
$rows = $query->fetchall();
foreach ($rows as $post) {
$created = $post['date'] / 1000;
$submit->execute(array(
$post['post_id'],
$post['user_id'],
$post['content'],
$created
));
}
}
function migrate_comments() {
echo "migrating comments\n";
extract($GLOBALS);
// load queries
$query = $sqlite->prepare(
'SELECT comment_id, user_id, post_id, content, date FROM comments;'
);
$submit = $psql->prepare(
'INSERT INTO admin.comment
(id, user_id, post_id, content, created)
VALUES
(?, ?, ?, ?, to_timestamp(?));'
);
// load comments
$query->execute();
$rows = $query->fetchall();
foreach ($rows as $comment) {
$created = $comment['date'] / 1000;
$submit->execute(array(
$comment['comment_id'],
$comment['user_id'],
$comment['post_id'],
$comment['content'],
$created
));
}
}
function migrate_likes() {
echo "migrating likes\n";
extract($GLOBALS);
// load queries
$query = $sqlite->prepare(
'SELECT user_id, post_id FROM likes;'
);
$submit = $psql->prepare(
'INSERT INTO admin.like
(user_id, post_id)
VALUES
(?, ?);'
);
// load likes
$query->execute();
$rows = $query->fetchall();
foreach ($rows as $like) {
$submit->execute(array(
$like['user_id'],
$like['post_id']
));
}
}
function migrate_follow() {
echo "migrating follow\n";
extract($GLOBALS);
// load queries
$query = $sqlite->prepare(
'SELECT follower_id, followee_id FROM friends;'
);
$submit = $psql->prepare(
'INSERT INTO admin.follow
(follower_id, followee_id)
VALUES
(?, ?);'
);
// load follows
$query->execute();
$rows = $query->fetchall();
foreach ($rows as $follow) {
$submit->execute(array(
$follow['follower_id'],
$follow['followee_id']
));
}
}
function migrate_user_media($type) {
echo "migrating user media ($type)\n";
extract($GLOBALS);
// load queries
$submit = $psql->prepare(
'INSERT INTO admin.user_media
(user_id, content, mime, type)
VALUES
(?, decode(?, \'base64\'), ?, ?);'
);
// get dir
$dir = "/data/custom/{$type}";
if (!is_dir($dir)) {
return;
}
// load user media
if ($handle = opendir($dir)) {
while (FALSE !== ($file = readdir($handle))) {
if ('.' === $file) continue;
if ('..' === $file) continue;
$n = strpos($file, ".png");
$uid = substr($file, 0, $n);
$path = "{$dir}/{$file}";
$data = base64_encode(file_get_contents($path));
$submit->execute(array(
$uid,
$data,
'image/png',
$type
));
}
closedir($handle);
}
}
try {
wait_until_ready();
clear_all();
migrate_users();
migrate_posts();
migrate_comments();
migrate_likes();
migrate_follow();
migrate_user_media('avatar');
migrate_user_media('banner');
} catch (Exception $ex) {
echo "$ex\n";
$psql->rollBack();
die();
}
$psql->commit();

View file

@ -0,0 +1,41 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Settings_controller extends Controller {
// the home model
private $settings_model;
// the format model
protected $format_model;
// the post controller
protected $post_controller;
// the people controller
protected $people_controller;
function __construct($load) {
parent::__construct($load);
$this->settings_model = $this->load->model('apps/settings');
}
public function index(): void {
if (!$this->main->session) {
$this->redirect('/auth/login');
}
parent::index();
$data = $this->settings_model->get_data();
if (!$data) {
$this->error(404);
}
$this->load->app_lang($this->main->info['lang'], 'auth');
$this->view('header', $data);
$this->view('apps/settings/main', $data);
$this->view('footer', $data);
}
}
?>

View file

@ -21,6 +21,10 @@ class Modal_controller extends Controller {
$this->modal('new_post');
}
public function about(): void {
$this->modal('about');
}
public function register(): void {
$this->load->app_lang(
$this->main->info['lang'],

View file

@ -13,7 +13,8 @@ class Template_controller extends Controller {
$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)
'hint' => $this->request_model->get_str('hint', FALSE),
'type' => $this->request_model->get_str('type', 'error')
);
$this->view('template/toast', $data);
}

View file

@ -0,0 +1,16 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Settings_model extends Model {
private $request_model;
function __construct($load) {
parent::__construct($load);
$this->request_model = $this->load->model('request');
}
public function get_data(): ?array {
$data = parent::get_data();
$data['title'] = ucfirst(lang('title'));
return $data;
}
}

View file

@ -3,7 +3,7 @@
<div id="main-content">
<div id="profile-header-container">
<div id="profile-header" class="col">
<?=image('/api/rpc/profile_banner?user_id=' . $user['id'], 'banner')?>
<?=image('/api/rpc/profile_banner?user_id=' . $user['id'], 'banner', mime: $user['banner_mime'])?>
<div class="info row">
<div class="pfp-wrapper">
<?=pfp($user)?>

View file

@ -0,0 +1,151 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$user = $this->main->user();
function __create_form($user, $col) {
$ph = ucfirst(lang('ph_' . $col));
$val = $user[$col];
return "<form action=\"\" class=\"row mt settings-form\" onsubmit=\"handleSubmit(event)\">
<div class=\"rel mb\" style=\"flex: 1\">
<input
type=\"text\"
name=\"{$col}\"
id=\"{$col}\"
placeholder=\" \"
value=\"{$val}\"
>
<label for=\"{$col}\">
{$ph}
</label>
</div>
<input type=\"hidden\" name=\"col\" value=\"{$col}\">
<input type=\"hidden\" name=\"uid\" value=\"{$user['id']}\">
<button
class=\"btn btn-submit ml\"
style=\"flex: 0; height: fit-content;\"
><i class=\"mi\">check</i></button>
</form>";
}
?>
<script>
function onSuccess() {
successToast(<?=json_encode(ucfirst(lang('settings_success')))?>);
}
function handleSubmit(e) {
e.preventDefault();
let el = e.target.elements;
let col = el.col.value;
let uid = el.uid.value;
let val = el[col].value;
$.ajax({
url: '/api/user?id=eq.' + uid,
method: 'PATCH',
data: JSON.stringify({ [col]: val }),
success: onSuccess
});
}
const toBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});
function updateMedia(media_type) {
var input = document.createElement('input');
input.type = 'file';
input.onchange = async (e) => {
var file = e.target.files[0];
var data = (await toBase64(file)).split(";");
var mime = data[0].split(":")[1];
var content = data[1].split(",")[1];
$.ajax({
url: '/api/rpc/update_user_media',
method: 'POST',
data: JSON.stringify({
media_type, mime, content
}),
success: onSuccess
});
}
input.click();
}
function resetMedia(media_type) {
$.ajax({
url: '/api/rpc/delete_user_media',
method: 'POST',
data: JSON.stringify({
media_type
}),
success: onSuccess
});
}
</script>
<div id="main-content">
<div id="settings" class="card">
<h1><?=ucfirst(lang('title'))?></h1>
<hr class="mt">
<h2><?=ucfirst(lang('general_title'))?></h2>
<strong><?=ucfirst(lang('general_desc'))?></strong>
<?=__create_form($user, 'username')?>
<?=__create_form($user, 'email')?>
<?=__create_form($user, 'first_name')?>
<?=__create_form($user, 'last_name')?>
<?=__create_form($user, 'gender')?>
<hr class="mt">
<h2><?=ucfirst(lang('media_title'))?></h2>
<strong><?=ucfirst(lang('media_desc'))?></strong>
<h3><?=ucfirst(lang('ph_avatar'))?></h3>
<div class="row">
<?=image(
"/api/rpc/profile_avatar?user_id={$user['id']}",
height: '100px'
)?>
<div class="col ml">
<button
class="btn btn-alt btn-blue"
onclick="updateMedia('avatar')"
><?=ucfirst(lang('update'))?></button>
<button
class="btn btn-alt btn-blue mt"
onclick="resetMedia('avatar')"
><?=ucfirst(lang('reset'))?></button>
</div>
</div>
<h3><?=ucfirst(lang('ph_banner'))?></h3>
<div class="row">
<?=image(
"/api/rpc/profile_banner?user_id={$user['id']}",
height: '100px'
)?>
<div class="col ml">
<button
class="btn btn-alt btn-blue"
onclick="updateMedia('banner')"
><?=ucfirst(lang('update'))?></button>
<button
class="btn btn-alt btn-blue mt"
onclick="resetMedia('banner')"
><?=ucfirst(lang('reset'))?></button>
</div>
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<footer>
Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
<?=ucfirst(lang('copyright'))?> | <a href="https://freya.cat">freya.cat</a>
</footer>
<body>

View file

@ -55,7 +55,7 @@
</script>
<?=pfp($self, FALSE, 'toggleUserMenu()')?>
<div class="card col hidden" id="user-menu">
<span class="row" id="user-menu-header">
<span class="row mr" id="user-menu-header">
<?=pfp($self, FALSE)?>
<span class="col">
<strong><?=$this->format_model->name($self)?></strong>
@ -65,16 +65,25 @@
<hr>
<?=ilang(
'action_profile',
id: 'action-profile',
class: 'btn',
href: '/profile?id=' . $self['id']
)?>
<?=ilang(
'action_xssbook_about',
id: 'action-xssbook-about',
class: 'btn',
click: 'viewAbout'
)?>
<?=ilang(
'action_settings',
id: 'action-settings',
class: 'btn',
href: '/settings'
)?>
<?=ilang(
'action_logout',
id: 'action-logout',
class: 'btn',
href: '/auth/logout'
)?>
@ -84,6 +93,11 @@
<?php endif; ?>
</div>
<script>
$('#action-xssbook-about').on('click', function() {
$.get( "/modal/about", function (data) {
$(document.body).append(data);
});
})
$('#action-hamburger').on('click', function() {
let menu = $('.nav-center');
menu.toggleClass('visible');
@ -94,7 +108,6 @@
userMenu = $('#user-menu');
var nav = $('.nav');
document.onclick = function(event) {
console.log(event.target, nav[0]);
let outside = !(nav[0].contains(event.target));
if (outside) {
userMenu.addClass('hidden');

View file

@ -0,0 +1,28 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="about-modal-body">
<span class="logo">xssbook</span>
<hr>
<span class="mb"><?=ucfirst(lang('version'))?></span>
<span><?=ucfirst(lang('copyright'))?></span>
<hr>
<a class="btn btn-blue" href="https://g.freya.cat/freya/xssbook2">Source Code</a>
</div>
<style>
#about-modal-body {
display: flex;
margin-top: 50px;
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 1.1rem;
padding: 1rem;
}
#about-modal-body .logo {
color: var(--blue);
font-family: facebook;
font-size: 2.25rem;
margin-bottom: 1rem;
}
</style>

View file

@ -20,7 +20,7 @@
}
?>
<div class="toast error">
<div class="toast <?=$type?>">
<?=$lang_msg?>
<?=ilang('action_close', class: 'action-close-toast')?>
</div>

View file

@ -49,6 +49,11 @@ class Aesthetic {
'css/post.css'
],
),
'settings' => array(
'css' => [
'css/settings.css'
]
),
);
}
/**

View file

@ -6,5 +6,6 @@ $routes['error'] = 'apps/error';
$routes['auth'] = 'apps/auth';
$routes['people'] = 'apps/people';
$routes['profile'] = 'apps/profile';
$routes['settings'] = 'apps/settings';
$routes[''] = '_index';

View file

@ -1,6 +1,14 @@
<?php /* Copyright (c) 2024 Freya Murphy */
function image($src, $class = NULL, $link = NULL, $click = NULL): string {
function image(
$src,
$class = NULL,
$link = NULL,
$click = NULL,
$height = NULL,
$width = NULL,
$mime = NULL,
): string {
if ($class) {
$class = 'image-loading ' . $class;
} else {
@ -9,6 +17,11 @@ function image($src, $class = NULL, $link = NULL, $click = NULL): string {
$content = '';
// dont need mime for images
if ($mime && strpos($mime, 'image') !== FALSE) {
$mime = NULL;
}
if ($link) {
$content .= '<a class="' . $class . '" href="' . $link . '">';
} else if ($click) {
@ -16,7 +29,22 @@ function image($src, $class = NULL, $link = NULL, $click = NULL): string {
} else {
$content .= '<span class="' . $class . '">';
}
$content .= '<img src="' . $src . '" onerror="onImgError(this)" onload="onImgLoad(this)"/>';
if ($mime) {
$content .= '<object class="inner" type="' . $mime . '" data="' . $src . '" ';
} else {
$content .= '<img class="inner" src="' . $src . '" ';
}
if ($height) {
$content .= "height=\"{$height}\" ";
}
if ($width) {
$content .= "width=\"{$width}\" ";
}
if ($mime) {
$content .= '></object>';
} else {
$content .= 'onerror="onImgError(this)" onload="onImgLoad(this)"/>';
}
if ($link) {
$content .= '</a>';
} else if ($click) {
@ -36,5 +64,14 @@ function pfp(
if ($link === TRUE) {
$link = '/profile?id=' . $user['id'];
}
return image('/api/rpc/profile_avatar?user_id=' . $user['id'], 'pfp', link: $link, click: $click);
$mime = NULL;
if (isset($user['avatar_mime'])) {
$mime = $user['avatar_mime'];
}
return image('/api/rpc/profile_avatar?user_id=' . $user['id'],
'pfp',
link: $link,
click: $click,
mime: $mime
);
}

View file

@ -0,0 +1,17 @@
<?php
$lang['title'] = 'Settings';
$lang['settings_success'] = 'Updated successfully';
$lang['general_title'] = 'Account Information';
$lang['general_desc'] = 'Modify your general account information.';
$lang['media_title'] = 'Account Media';
$lang['media_desc'] = 'Modify your profiles avatar and banner.';
$lang['ph_avatar'] = 'Avatar';
$lang['ph_banner'] = 'Banner';
$lang['update'] = 'Update';
$lang['reset'] = 'Reset';

View file

@ -1,5 +1,8 @@
<?php /* Copyright (c) 2024 Freya Murphy */
$lang['version'] = 'Version 2.0.0';
$lang['copyright'] = 'Freya Murphy © 2024';
// Navigation Bar Lang
$lang['action_home_text'] = 'Home';
$lang['action_home_tip'] = 'Goto your home page.';
@ -44,11 +47,17 @@ $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.';
$lang['about_modal_title'] = 'XSSBook';
// User Menu
$lang['action_logout_text'] = 'Logout';
$lang['action_logout_tip'] = 'Logout';
$lang['action_logout_icon'] = 'mi mi-sm';
$lang['action_logout_content'] = 'logout';
$lang['action_xssbook_about_text'] = 'About';
$lang['action_xssbook_about_tip'] = 'View xssbook about modal';
$lang['action_xssbook_about_icon'] = 'mi mi-sm';
$lang['action_xssbook_about_content'] = 'info';
$lang['action_settings_text'] = 'Settings';
$lang['action_settings_tip'] = 'Edit account settings';
$lang['action_settings_icon'] = 'mi mi-sm';