v2 done
This commit is contained in:
parent
4c8d58b646
commit
708594d32f
34 changed files with 854 additions and 51 deletions
10
LICENSE
Normal file
10
LICENSE
Normal 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
39
README.md
Normal 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
|
|
@ -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"]
|
|
@ -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
9
build/shim/Dockerfile
Normal 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"]
|
|
@ -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
26
src/db/rest/media/api_delete_user_media.sql
Normal file
26
src/db/rest/media/api_delete_user_media.sql
Normal 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;
|
41
src/db/rest/media/api_update_user_media.sql
Normal file
41
src/db/rest/media/api_update_user_media.sql
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.profile .pfp, .profile .pfp img {
|
||||
.profile .pfp, .profile .pfp .inner {
|
||||
padding: none;
|
||||
margin: none;
|
||||
width: 100%;
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
#post-container {
|
||||
max-width: 40rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post, #new-post {
|
||||
margin-bottom: 1rem;
|
||||
max-width: 40rem;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
13
src/public/css/settings.css
Normal file
13
src/public/css/settings.css
Normal 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%;
|
||||
}
|
|
@ -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
285
src/shim/shim.php
Executable 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();
|
41
src/web/_controller/apps/settings.php
Normal file
41
src/web/_controller/apps/settings.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
|
@ -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'],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
16
src/web/_model/apps/settings.php
Normal file
16
src/web/_model/apps/settings.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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)?>
|
||||
|
|
151
src/web/_views/apps/settings/main.php
Normal file
151
src/web/_views/apps/settings/main.php
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
28
src/web/_views/modal/about.php
Normal file
28
src/web/_views/modal/about.php
Normal 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>
|
|
@ -20,7 +20,7 @@
|
|||
}
|
||||
|
||||
?>
|
||||
<div class="toast error">
|
||||
<div class="toast <?=$type?>">
|
||||
<?=$lang_msg?>
|
||||
<?=ilang('action_close', class: 'action-close-toast')?>
|
||||
</div>
|
||||
|
|
|
@ -49,6 +49,11 @@ class Aesthetic {
|
|||
'css/post.css'
|
||||
],
|
||||
),
|
||||
'settings' => array(
|
||||
'css' => [
|
||||
'css/settings.css'
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
17
src/web/lang/en_US/apps/settings.php
Normal file
17
src/web/lang/en_US/apps/settings.php
Normal 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';
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue