initial
This commit is contained in:
parent
5bd52bc7fd
commit
de9cae795f
36 changed files with 2931 additions and 0 deletions
34
base.env
Normal file
34
base.env
Normal file
|
@ -0,0 +1,34 @@
|
|||
# ============================= GENERAL ==
|
||||
|
||||
# project name
|
||||
PROJECT_NAME="crimson"
|
||||
|
||||
# ================================= WEB ==
|
||||
|
||||
# What port to listen on
|
||||
HTTP_BIND=127.0.0.1
|
||||
HTTP_PORT=80
|
||||
|
||||
# ============================ DATABASE ==
|
||||
|
||||
# Do we want the postgres database enabled
|
||||
POSTGRES_ENABLED=false
|
||||
|
||||
# Database authentication
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
|
||||
# ================================= API ==
|
||||
|
||||
# Do we want the postgrest api enabled
|
||||
API_ENABLED=false
|
||||
|
||||
# API Jwt Secret
|
||||
API_SECRET="<base64>"
|
||||
|
||||
# API Anonymous role
|
||||
API_ROLE="anonymous"
|
||||
|
||||
# API schema
|
||||
API_SCHEMA="api"
|
41
build/db-init/Dockerfile
Normal file
41
build/db-init/Dockerfile
Normal file
|
@ -0,0 +1,41 @@
|
|||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
FROM alpine:latest
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache postgresql16-client tini shadow sed
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# setup main user
|
||||
RUN adduser -D db-init
|
||||
RUN groupmod --gid 1000 db-init
|
||||
RUN usermod --uid 1000 db-init
|
||||
|
||||
# copy scripts
|
||||
COPY ./db-init /usr/local/bin/db-init
|
||||
COPY ./rev.sql /var/lib/rev.sql
|
||||
COPY ./ext.sql /var/lib/ext.sql
|
||||
COPY ./base.sql /var/lib/base.sql
|
||||
|
||||
# remove build packages
|
||||
RUN apk del shadow
|
||||
|
||||
# do the
|
||||
USER db-init
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/usr/local/bin/db-init"]
|
52
build/db-init/README.md
Normal file
52
build/db-init/README.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
## db-init
|
||||
|
||||
This script setups of the databse with the requrired baseline, runs migrations,
|
||||
and loads the api schema (if enabled).
|
||||
|
||||
#### Migration script
|
||||
|
||||
All migrations scrips MUST do ALL of the following:
|
||||
|
||||
- Placed in src/db/migrations
|
||||
- Named with its migration number (0 indexed), and have four
|
||||
numbers of padding. i.e. `0000.sql`, `0030.sql`, or `9999.sql`
|
||||
- In numerical order with all other migrations (cannot go from migration
|
||||
0 to 2).
|
||||
- A postgres transaction. `BEGIN TRANSACTION ... COMMIT TRANSACTION`.
|
||||
- End with the following before COMMIT, where <rev> is the NEXT
|
||||
revision number. (i.e. in `0000.sql` <rev> MUST be 1).
|
||||
|
||||
```
|
||||
UPDATE sys.database_info SET curr_revision = <rev> WHERE name = current_database();
|
||||
```
|
||||
|
||||
Example `0000.sql`:
|
||||
```sql
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE SCHEMA website;
|
||||
|
||||
UPDATE sys.database_info SET curr_revision = 1 WHERE name = current_database();
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
```
|
||||
|
||||
Migrations will ONLY EVER be ONCE, and will ALLWAYS be run in order. This means
|
||||
that you can assume all previous migrations have run successfully in any current
|
||||
migration, and there is NO other possible state in the database.
|
||||
|
||||
### API
|
||||
|
||||
Once all migrations have been completed, the api will be initalized (if enabled.
|
||||
|
||||
If you opt to use postgrest which is builtin into crimson, you must create a
|
||||
sql file loads the api schema that MUST do ALL of the following:
|
||||
|
||||
- Placed in src/db/rest and named `rest.sql`
|
||||
- A postgres transaction. `BEGIN TRANSACTION ... COMMIT TRANSACTION`.
|
||||
|
||||
Within that transaction you can setup postgres with the api schema you want.
|
||||
See https://docs.postgrest.org/en/v12/. (crimson currently uses postgres 12).
|
||||
|
||||
NOTE: If you want to load any sql file though an absolute path, src/db will be
|
||||
mounted as READ ONLY to /db. (i.e. src/db/rest/rest.sql => /db/rest/rest.sql).
|
86
build/db-init/base.sql
Normal file
86
build/db-init/base.sql
Normal file
|
@ -0,0 +1,86 @@
|
|||
--- CRIMSON --- A simple PHP framework.
|
||||
--- Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
---
|
||||
--- This file is part of CRIMSON.
|
||||
---
|
||||
--- CRIMSON is free software; you can redistribute it and/or modify it
|
||||
--- under the terms of the GNU General Public License as published by
|
||||
--- the Free Software Foundation; either version 3 of the License, or (at
|
||||
--- your option) any later version.
|
||||
---
|
||||
--- CRIMSON is distributed in the hope that it will be useful, but
|
||||
--- WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--- GNU General Public License for more details.
|
||||
---
|
||||
--- You should have received a copy of the GNU General Public License
|
||||
--- along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-- Set's up crimsons baseline database. Can be run multiple times.
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
SET search_path = public;
|
||||
SET client_min_messages TO WARNING;
|
||||
|
||||
-- Migration Start
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS sys;
|
||||
|
||||
-- sys
|
||||
|
||||
ALTER SCHEMA sys OWNER TO POSTGRES_USER;
|
||||
|
||||
-- sys.*/*
|
||||
|
||||
DROP DOMAIN IF EXISTS sys."*/*" CASCADE;
|
||||
CREATE DOMAIN sys."*/*" AS BYTEA;
|
||||
|
||||
-- sys.database_info
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys.database_info (
|
||||
name TEXT DEFAULT ''::text NOT NULL,
|
||||
jwt_secret TEXT DEFAULT ''::text NOT NULL,
|
||||
curr_revision INTEGER DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE sys.database_info
|
||||
DROP CONSTRAINT IF EXISTS database_info_pkey;
|
||||
|
||||
ALTER TABLE sys.database_info
|
||||
ADD CONSTRAINT database_info_pkey PRIMARY KEY (name);
|
||||
|
||||
ALTER TABLE sys.database_info OWNER TO POSTGRES_USER;
|
||||
|
||||
INSERT INTO sys.database_info
|
||||
(name, curr_revision) VALUES (current_database(), 0)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- sys.JWT
|
||||
|
||||
DROP TYPE IF EXISTS sys.JWT CASCADE;
|
||||
CREATE TYPE sys.JWT AS (
|
||||
token TEXT
|
||||
);
|
||||
|
||||
-- authenticator
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT * FROM pg_user WHERE usename = 'authenticator')
|
||||
THEN
|
||||
CREATE ROLE authenticator
|
||||
LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER ROLE authenticator WITH PASSWORD 'postgrest';
|
||||
|
||||
-- anonymous
|
||||
|
||||
DROP ROLE IF EXISTS anonymous;
|
||||
CREATE ROLE anonymous NOLOGIN;
|
||||
|
||||
-- Migration End;
|
||||
|
||||
COMMIT TRANSACTION;
|
168
build/db-init/db-init
Executable file
168
build/db-init/db-init
Executable file
|
@ -0,0 +1,168 @@
|
|||
#!/bin/sh
|
||||
|
||||
errors=$(mktemp)
|
||||
|
||||
step() {
|
||||
printf '\x1b[34;1m>> %s\x1b[0m\n' "$*"
|
||||
}
|
||||
|
||||
error() {
|
||||
{
|
||||
printf '\x1b[31;1merror: \x1b[0m%s\n' "$*";
|
||||
grep -v 'current transaction is aborted' < "$errors";
|
||||
printf "\x1b[31m;1error: \x1b[0mAborting migrations, fix file(s) then restart process.";
|
||||
} 1>&2;
|
||||
}
|
||||
|
||||
try() {
|
||||
"$@" 2> "$errors";
|
||||
count=$(grep -c 'ERROR' < "$errors")
|
||||
if [ "$count" -eq 0 ]; then
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
fi
|
||||
}
|
||||
|
||||
export PGPASSWORD="$POSTGRES_PASSWORD"
|
||||
|
||||
psql() {
|
||||
/usr/bin/psql \
|
||||
-h postgres \
|
||||
-p 5432 \
|
||||
-d "$POSTGRES_DB" \
|
||||
-U "$POSTGRES_USER" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
pg_isready() {
|
||||
/usr/bin/pg_isready \
|
||||
-h postgres \
|
||||
-p 5432 \
|
||||
-d "$POSTGRES_DB" \
|
||||
-U "$POSTGRES_USER"
|
||||
}
|
||||
|
||||
curr_revision() {
|
||||
sed "s/POSTGRES_USER/$POSTGRES_USER/g" /var/lib/rev.sql > /tmp/rev.sql
|
||||
psql -qtAX -f /tmp/rev.sql;
|
||||
}
|
||||
|
||||
wait_until_ready() {
|
||||
step 'Checking if the database is ready...';
|
||||
while true; do
|
||||
pg_isready;
|
||||
code=$?;
|
||||
if [ $code -eq 0 ]; then
|
||||
break;
|
||||
fi
|
||||
sleep 3;
|
||||
done
|
||||
}
|
||||
|
||||
run_hook() {
|
||||
hook="$1"
|
||||
if [ ! -f "$hook" ]; then
|
||||
printf '\x1b[31;1merror: \x1b[0m%s\n' "required hook not found: '$hook'";
|
||||
return 1;
|
||||
fi
|
||||
if ! try psql -f "$hook"; then
|
||||
error "An error occoured during a hook ($hook)"
|
||||
return 1;
|
||||
fi
|
||||
}
|
||||
|
||||
run_baseline() {
|
||||
sed "s/POSTGRES_USER/$POSTGRES_USER/g" /var/lib/base.sql > /tmp/base.sql
|
||||
run_hook /tmp/base.sql
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
i="$1"
|
||||
while true; do
|
||||
name=$(printf "%04d" "$i");
|
||||
file="/db/migrations/$name.sql"
|
||||
if [ -f "$file" ]; then
|
||||
if try psql -f "$file"; then
|
||||
i=$((i+1));
|
||||
continue;
|
||||
else
|
||||
error "An error occoured during a migration (rev $name)"
|
||||
return 1;
|
||||
fi
|
||||
else
|
||||
return 0;
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
update_jwt() {
|
||||
if try psql -c "UPDATE sys.database_info SET jwt_secret = '$API_SECRET' WHERE name = current_database();"; then
|
||||
return 0;
|
||||
else
|
||||
error "Could not update JWT"
|
||||
return 1;
|
||||
fi
|
||||
}
|
||||
|
||||
init_api () {
|
||||
step 'Initalizing the api';
|
||||
# reinit the api schema for
|
||||
# postgrest
|
||||
if ! run_hook "/db/rest/rest.sql"; then
|
||||
return 1;
|
||||
fi
|
||||
|
||||
step 'Updating JWT secret';
|
||||
# make sure postgres has the corrent
|
||||
# jwt secret
|
||||
if ! update_jwt; then
|
||||
return 1;
|
||||
fi
|
||||
}
|
||||
|
||||
init_db () {
|
||||
# reomve ready status
|
||||
# so php ignores requests
|
||||
rm -f /var/run/crimson/db_ready
|
||||
|
||||
step 'Waiting for database';
|
||||
# make sure the database is running
|
||||
# before we run any requests
|
||||
wait_until_ready;
|
||||
step 'Database ready';
|
||||
|
||||
step 'Loading extensions';
|
||||
# Make sure extensions are loaded
|
||||
if ! run_hook "/var/lib/ext.sql"; then
|
||||
return 1;
|
||||
fi
|
||||
|
||||
step 'Checking baseline'
|
||||
# make sure baseline sys schema exists
|
||||
if ! run_baseline; then
|
||||
return 1;
|
||||
fi
|
||||
|
||||
step 'Peforming migrations';
|
||||
# get the current revision
|
||||
REV=$(curr_revision);
|
||||
step "Database at revision: $REV"
|
||||
# run each migration that is
|
||||
# higher than our current revision
|
||||
if ! run_migrations "$REV"; then
|
||||
return 1;
|
||||
fi
|
||||
|
||||
if [ "$API_ENABLED" = "true" ]; then
|
||||
if ! init_api; then
|
||||
return 1;
|
||||
fi
|
||||
fi
|
||||
|
||||
step 'Database is initialized'
|
||||
# database is ready
|
||||
touch /var/run/crimson/db_ready
|
||||
}
|
||||
|
||||
init_db
|
28
build/db-init/ext.sql
Normal file
28
build/db-init/ext.sql
Normal file
|
@ -0,0 +1,28 @@
|
|||
--- CRIMSON --- A simple PHP framework.
|
||||
--- Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
---
|
||||
--- This file is part of CRIMSON.
|
||||
---
|
||||
--- CRIMSON is free software; you can redistribute it and/or modify it
|
||||
--- under the terms of the GNU General Public License as published by
|
||||
--- the Free Software Foundation; either version 3 of the License, or (at
|
||||
--- your option) any later version.
|
||||
---
|
||||
--- CRIMSON is distributed in the hope that it will be useful, but
|
||||
--- WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--- GNU General Public License for more details.
|
||||
---
|
||||
--- You should have received a copy of the GNU General Public License
|
||||
--- along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-- Loads required postgres extensions.
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
SET search_path = public;
|
||||
SET client_min_messages TO WARNING;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS pgjwt;
|
||||
|
||||
COMMIT TRANSACTION;
|
41
build/db-init/rev.sql
Normal file
41
build/db-init/rev.sql
Normal file
|
@ -0,0 +1,41 @@
|
|||
--- CRIMSON --- A simple PHP framework.
|
||||
--- Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
---
|
||||
--- This file is part of CRIMSON.
|
||||
---
|
||||
--- CRIMSON is free software; you can redistribute it and/or modify it
|
||||
--- under the terms of the GNU General Public License as published by
|
||||
--- the Free Software Foundation; either version 3 of the License, or (at
|
||||
--- your option) any later version.
|
||||
---
|
||||
--- CRIMSON is distributed in the hope that it will be useful, but
|
||||
--- WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--- GNU General Public License for more details.
|
||||
---
|
||||
--- You should have received a copy of the GNU General Public License
|
||||
--- along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-- Gets the current databse revision.
|
||||
|
||||
CREATE OR REPLACE FUNCTION curr_revision()
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql VOLATILE
|
||||
AS $BODY$
|
||||
DECLARE
|
||||
_revision INTEGER;
|
||||
BEGIN
|
||||
BEGIN
|
||||
SELECT curr_revision INTO _revision
|
||||
FROM sys.database_info
|
||||
WHERE name = current_database();
|
||||
RETURN _revision;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN 0;
|
||||
END;
|
||||
END
|
||||
$BODY$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION curr_revision() TO POSTGRES_USER;
|
||||
|
||||
SELECT curr_revision();
|
22
build/init/Dockerfile
Normal file
22
build/init/Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
FROM alpine:latest
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache tini shadow coreutils findutils
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# setup main user
|
||||
RUN adduser -D init
|
||||
RUN groupmod --gid 1000 init
|
||||
RUN usermod --uid 1000 init
|
||||
|
||||
# copy scripts
|
||||
COPY ./init /usr/local/bin/init
|
||||
COPY ./stamp.sh /usr/local/bin/stamp.sh
|
||||
|
||||
# remove build packages
|
||||
RUN apk del shadow
|
||||
|
||||
# do the
|
||||
USER init
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/usr/local/bin/init"]
|
4
build/init/init
Executable file
4
build/init/init
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
# stamp assets
|
||||
/usr/local/bin/stamp.sh
|
14
build/init/stamp.sh
Executable file
14
build/init/stamp.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
out="/var/run/crimson/stamp.php"
|
||||
public="/opt/site/public"
|
||||
files=$(find "$public" -type f -printf %P\\n)
|
||||
|
||||
printf "<?php\n\$__stamps = array();\n" > "$out"
|
||||
for file in $files; do
|
||||
stamp=$(date +%s -r "$public/$file")
|
||||
echo "\$__stamps['public/$file'] = $stamp;" >> "$out";
|
||||
done
|
||||
echo "define('FILE_TIMES', \$__stamps);" >> "$out"
|
||||
echo "unset(\$__stamps);" >> "$out"
|
||||
|
30
build/nginx/Dockerfile
Normal file
30
build/nginx/Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
|||
FROM alpine:latest
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache nginx shadow curl tini
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# update nginx user
|
||||
RUN groupmod --gid 1000 nginx
|
||||
RUN usermod --uid 1000 nginx
|
||||
|
||||
# remove build packages
|
||||
RUN apk del shadow
|
||||
|
||||
# make log syms
|
||||
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
|
||||
ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
# copy configs
|
||||
RUN mkdir -p /etc/nginx
|
||||
COPY ./*.conf /etc/nginx/
|
||||
RUN chown -R nginx:nginx /etc/nginx
|
||||
|
||||
# copy entrypoint
|
||||
COPY ./entrypoint.sh /usr/local/bin/entrypoint
|
||||
RUN chmod +x /usr/local/bin/entrypoint
|
||||
|
||||
# do the
|
||||
USER nginx
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/usr/local/bin/entrypoint"]
|
8
build/nginx/entrypoint.sh
Executable file
8
build/nginx/entrypoint.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ ! "$API_ENABLED" = "true" ]; then
|
||||
echo "" > /etc/nginx/nginx.api.conf
|
||||
echo "" > /etc/nginx/nginx.api.server.conf
|
||||
fi
|
||||
|
||||
exec -a /usr/sbin/nginx /usr/sbin/nginx -c /etc/nginx/nginx.conf
|
3
build/nginx/nginx.api.conf
Normal file
3
build/nginx/nginx.api.conf
Normal file
|
@ -0,0 +1,3 @@
|
|||
upstream postgrest {
|
||||
server rest:3000;
|
||||
}
|
14
build/nginx/nginx.api.server.conf
Normal file
14
build/nginx/nginx.api.server.conf
Normal file
|
@ -0,0 +1,14 @@
|
|||
location /api/ {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_redirect off;
|
||||
|
||||
default_type application/json;
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header Content-Location /api/$upstream_http_content_location;
|
||||
proxy_set_header Connection "";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_pass http://postgrest/;
|
||||
}
|
54
build/nginx/nginx.conf
Normal file
54
build/nginx/nginx.conf
Normal file
|
@ -0,0 +1,54 @@
|
|||
worker_processes 4;
|
||||
daemon off;
|
||||
pid /tmp/nginx.pid;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 70;
|
||||
server_tokens off;
|
||||
client_max_body_size 2m;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
include "nginx.api.conf";
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
root /opt/site;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;
|
||||
|
||||
include "nginx.api.server.conf";
|
||||
|
||||
location /favicon.ico {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
root /opt/site/public;
|
||||
}
|
||||
|
||||
location /public {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header Content-Security-Policy "base-uri 'none'";
|
||||
root /opt/crimson;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
}
|
||||
}
|
||||
}
|
17
build/php/Dockerfile
Normal file
17
build/php/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM php:fpm-alpine
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache postgresql-dev runuser shadow
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# update php user
|
||||
RUN groupmod --gid 1000 www-data
|
||||
RUN usermod --uid 1000 www-data
|
||||
|
||||
# install php packages
|
||||
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
|
||||
RUN docker-php-ext-install pdo pdo_pgsql
|
||||
|
||||
# remove build packages
|
||||
RUN apk del shadow
|
||||
USER www-data
|
24
build/postgres/Dockerfile
Normal file
24
build/postgres/Dockerfile
Normal file
|
@ -0,0 +1,24 @@
|
|||
FROM postgres:16-alpine
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache make git shadow
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# install pgjwt
|
||||
RUN git clone https://github.com/michelp/pgjwt.git /tmp/pgjwt
|
||||
WORKDIR /tmp/pgjwt
|
||||
RUN make install
|
||||
|
||||
# update postgres user
|
||||
RUN groupmod --gid 1000 postgres
|
||||
RUN usermod --uid 1000 postgres
|
||||
|
||||
# remove build packages
|
||||
RUN apk del make git shadow
|
||||
|
||||
# set perms
|
||||
RUN chown -R postgres:postgres /var/run/postgresql
|
||||
|
||||
# fix workdir
|
||||
WORKDIR /
|
||||
USER postgres
|
30
build/postgrest/Dockerfile
Normal file
30
build/postgrest/Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
|||
FROM alpine:latest
|
||||
|
||||
# install packages
|
||||
RUN apk add --no-cache tini wget curl shadow
|
||||
RUN rm -fr /var/cache/apk/*
|
||||
|
||||
# setup main user
|
||||
RUN adduser -D postgrest
|
||||
RUN groupmod --gid 1000 postgrest
|
||||
RUN usermod --uid 1000 postgrest
|
||||
|
||||
# install postgrest
|
||||
RUN wget "https://github.com/PostgREST/postgrest/releases/download/v12.2.3/postgrest-v12.2.3-linux-static-x64.tar.xz" -O /tmp/postgrest.tar.xz
|
||||
RUN tar xJf /tmp/postgrest.tar.xz -C /usr/local/bin
|
||||
RUN rm /tmp/postgrest.tar.xz
|
||||
|
||||
# copy scripts
|
||||
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
# remove build packages
|
||||
RUN apk del shadow
|
||||
|
||||
# make the dirs
|
||||
RUN mkdir -p /etc/postgrest.d && \
|
||||
chown postgrest:postgrest /etc/postgrest.d
|
||||
|
||||
# do the
|
||||
USER postgrest
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/usr/local/bin/entrypoint.sh"]
|
23
build/postgrest/entrypoint.sh
Executable file
23
build/postgrest/entrypoint.sh
Executable file
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
|
||||
config=/etc/postgrest.d/postgrest.conf
|
||||
|
||||
JWT_SECRET="$API_SECRET"
|
||||
|
||||
PGRST_DB_URI="postgres://authenticator:postgrest@postgres:5432/$POSTGRES_DB"
|
||||
PGRST_ROLE="$API_ROLE"
|
||||
PGRST_SCHEMA="$API_SCHEMA"
|
||||
|
||||
rm -fr "$config"
|
||||
touch "$config"
|
||||
{
|
||||
printf 'db-uri = "%s"\n' "$PGRST_DB_URI";
|
||||
printf 'db-anon-role = "%s"\n' "$PGRST_ROLE";
|
||||
printf 'db-schemas = "%s"\n' "$PGRST_SCHEMA";
|
||||
printf 'jwt-secret = "%s"\n' "$JWT_SECRET";
|
||||
printf 'jwt-secret-is-base64 = false\n';
|
||||
printf 'server-host = "*"\n';
|
||||
printf 'server-port = 3000\n';
|
||||
} >> $config
|
||||
|
||||
exec -a /usr/local/bin/postgrest /usr/local/bin/postgrest "$config"
|
122
compose
Executable file
122
compose
Executable file
|
@ -0,0 +1,122 @@
|
|||
#!/bin/sh
|
||||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# `compose`
|
||||
# This script will provide a docker compose interface with all the CRIMSON
|
||||
# environment setup. This script may request root privilages since it needs all
|
||||
# docker containers to run with user 1000:1000. If you are not user 1000:1000,
|
||||
# root privlage is required to create volume folders with uid/gid 1000.
|
||||
|
||||
# Make sure errors fail to avoid nasal demons
|
||||
set -e
|
||||
|
||||
# ========================================================= PERMISSION CHECK ==
|
||||
# Make sure we are either root or user 1000:1000. This is required for crimson.
|
||||
|
||||
args="$@"
|
||||
command="$0 $args"
|
||||
|
||||
uid=$(id -u)
|
||||
gid=$(id -g)
|
||||
|
||||
if [[ $uid -eq 1000 ]] && [[ $gid -eq 1000 ]]; then
|
||||
true # all set
|
||||
elif [[ $uid -eq 0 ]] && [[ $gid -eq 0 ]]; then
|
||||
true # all set
|
||||
else
|
||||
# root required (1000:1000 may not exist)
|
||||
exec sudo -E $command
|
||||
fi
|
||||
|
||||
# ================================================================ CONSTANTS ==
|
||||
# ROOT: This is the folder the crimson project is located in. ROOT is used to
|
||||
# access docker-compose files, base.env, and other crimson files.
|
||||
# CALL_ROOT: This is the folder that the user who called `compose` is currently
|
||||
# in. For crimson to work this must be the folder that your project using
|
||||
# crimson is. This is because crimson loads `.env` here to load any user
|
||||
# specified environment. `.env` is needed for $DATA and $SOURCE. Read base.env
|
||||
# for more information.
|
||||
|
||||
ROOT="$(realpath "$(dirname "$0")")"
|
||||
CALL_ROOT="$(pwd)"
|
||||
|
||||
# ================================================================ FUNCTIONS ==
|
||||
# add_arg - adds arguments to the docker compose command to be run
|
||||
# include_env - loads a .env file
|
||||
# include - adds the docker compose file to be included called
|
||||
# docker-compose.<$1>.yml
|
||||
|
||||
docker_args=""
|
||||
function add_arg {
|
||||
docker_args="$docker_args $@"
|
||||
}
|
||||
|
||||
function include_env {
|
||||
local file
|
||||
file="$1"
|
||||
if [ -f "$file" ]; then
|
||||
source "$file"
|
||||
add_arg --env-file $file
|
||||
fi
|
||||
}
|
||||
|
||||
function include {
|
||||
local file
|
||||
if [ "$2" = "true" ]; then
|
||||
file="$ROOT/docker/docker-compose.$1.yml"
|
||||
add_arg -f $file
|
||||
fi
|
||||
}
|
||||
|
||||
# ================================================================ BOOTSTRAP ==
|
||||
# Enter the crimson project directory, load all .env files, and pick which
|
||||
# docker-compose.*.yml files are requested. Then make the docker volumes here
|
||||
# with the correct permissions. If we let docker do it, it will make them owned
|
||||
# by root (thanks) and break our containers.
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
# get docker file includes
|
||||
include_env "$ROOT/base.env"
|
||||
include_env "$CALL_ROOT/.env"
|
||||
include "base" "true"
|
||||
include "db" "$POSTGRES_ENABLED"
|
||||
include "api" "$API_ENABLED"
|
||||
|
||||
# assert SOURCE and DATA are set
|
||||
if [ -z "$SOURCE" ]; then
|
||||
printf "fatal: SOURCE is not set. See '$ROOT/base.env'\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$DATA" ]; then
|
||||
printf "fatal: DATA is not set. See '$ROOT/base.env'\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# preset perms (postgres will crash if not)
|
||||
if [ ! -d "$DATA" ]; then
|
||||
mkdir -p "$DATA"
|
||||
mkdir -p "$DATA/crimson"
|
||||
mkdir -p "$DATA/schemas"
|
||||
chown -R 1000:1000 "$DATA"
|
||||
fi
|
||||
|
||||
# run docker compose
|
||||
exec -a docker -- docker compose -p $PROJECT_NAME $docker_args "$@"
|
60
docker/docker-compose.api.yml
Normal file
60
docker/docker-compose.api.yml
Normal file
|
@ -0,0 +1,60 @@
|
|||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
services:
|
||||
# `docker-compose-api.yml`
|
||||
# This compose file creates the `rest` container running postgrest. This is
|
||||
# the program that crimson uses for the default API functionality.
|
||||
#
|
||||
# This service stack is only enabled when API_ENABLED=true.
|
||||
#
|
||||
# WARNING: The postgres container must also be enabled `POSTGRES_ENABLED=true`
|
||||
# otherwise the docker stack will be invalid.
|
||||
|
||||
rest:
|
||||
# Postgrest uses postgres to store the api schema. To work properly, the
|
||||
# database must be working (healthy) and all the api schema must be loaded.
|
||||
# It is db-init's job to load the api schema, thus this container depends
|
||||
# on both postgres and db-init.
|
||||
build: ../build/postgrest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- API_SECRET
|
||||
- API_ROLE
|
||||
- API_SCHEMA
|
||||
- POSTGRES_DB
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
healthcheck:
|
||||
test: curl -I "http://localhost:3000/"
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 3s
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
db-init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
web:
|
||||
# Nginx proxies requests to the API making it an added dependency.
|
||||
depends_on:
|
||||
rest:
|
||||
condition: service_healthy
|
||||
|
72
docker/docker-compose.base.yml
Normal file
72
docker/docker-compose.base.yml
Normal file
|
@ -0,0 +1,72 @@
|
|||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
services:
|
||||
# `docker-compose-base.yml`
|
||||
# This compose file creates the `web`, `php`, and `init` containers.
|
||||
# This is the barebones stack required to run crimson.
|
||||
#
|
||||
# This service stack is ALWAYS enabled.
|
||||
|
||||
web:
|
||||
# Crimson runs a nginx proxy to facilitate requets to PHP and requests
|
||||
# to the API (if enabled). Since it needs PHP to be running to send
|
||||
# requests, `php` is an added dependency.
|
||||
#
|
||||
# HTTP_PORT and HTTP_BIND sets what the external listen address will be for
|
||||
# the entire crimson stack.
|
||||
build: ../build/nginx
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- API_ENABLED
|
||||
ports:
|
||||
- ${HTTP_BIND}:${HTTP_PORT}:8080
|
||||
volumes:
|
||||
- ${SOURCE}:/opt/site:ro
|
||||
- ../src:/opt/crimson:ro
|
||||
depends_on:
|
||||
php:
|
||||
condition: service_started
|
||||
|
||||
php:
|
||||
# There exists some crimson functionaly that MAY be used which requires a
|
||||
# stamp.php file to be auto generated. This is done in `init`, this `init`
|
||||
# is an added dependency.
|
||||
build: ../build/php
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
volumes:
|
||||
- ${SOURCE}:/opt/site:ro
|
||||
- ../src:/opt/crimson:ro
|
||||
- ${DATA}/crimson:/var/run/crimson
|
||||
depends_on:
|
||||
init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
init:
|
||||
# Initalizes required files for php. Currently init only generates stamp.php.
|
||||
# This file hols all file stamps for all public assets, which is used in
|
||||
# crimsons `asset_stamp` controller function.
|
||||
build: ../build/init
|
||||
restart: no
|
||||
volumes:
|
||||
- ${SOURCE}:/opt/site
|
||||
- ${DATA}/crimson:/var/run/crimson
|
80
docker/docker-compose.db.yml
Normal file
80
docker/docker-compose.db.yml
Normal file
|
@ -0,0 +1,80 @@
|
|||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
services:
|
||||
# `docker-compose-db.yml`
|
||||
# This compose file creates the `postgres`, and `db-init` containers.
|
||||
# Crimson uses prostgres for its database and uses db-init to initalize the
|
||||
# baseline required for all crimson components.
|
||||
#
|
||||
# This service stack is only enabled when API_ENABLED=true.
|
||||
|
||||
postgres:
|
||||
build: ../build/postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
- POSTGRES_DB
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
volumes:
|
||||
- ${DATA}/schemas:/var/lib/postgresql/data
|
||||
- ${SOURCE}/db:/db:ro
|
||||
healthcheck:
|
||||
test: pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 3s
|
||||
|
||||
db-init:
|
||||
# => Guarantees the required crimson baseline in the database.
|
||||
# i.e. sys schema used for storing migration meta data and other small
|
||||
# things.
|
||||
# => Runs all database migrations for you. All that is needed
|
||||
# is that the migration file convention is followed.
|
||||
# => If the API is enabled, also loads in the api schema for postgrest.
|
||||
#
|
||||
# Once db-init finishes, it will create a `db-ready` file in
|
||||
# /var/run/crimson to tell crimson's php code its to further route requests.
|
||||
#
|
||||
# For information on databse conventions and layouts, see
|
||||
# `build/db-init/README.md`.
|
||||
build: ../build/db-init
|
||||
restart: no
|
||||
environment:
|
||||
- POSTGRES_DB
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- API_ENABLED
|
||||
- API_SECRET
|
||||
volumes:
|
||||
- ${SOURCE}/db:/db:ro
|
||||
- ${DATA}/crimson:/var/run/crimson
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
php:
|
||||
# Crimson supports calling the postgres database inside php though the
|
||||
# database library. Thus is in added dependency. Crimson does not need to
|
||||
# depend on db-init since db-init has a ready file that crimons uses
|
||||
# instead.
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
45
psql
Executable file
45
psql
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/sh
|
||||
### CRIMSON --- A simple PHP framework.
|
||||
### Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
###
|
||||
### This file is part of CRIMSON.
|
||||
###
|
||||
### CRIMSON is free software; you can redistribute it and/or modify it
|
||||
### under the terms of the GNU General Public License as published by
|
||||
### the Free Software Foundation; either version 3 of the License, or (at
|
||||
### your option) any later version.
|
||||
###
|
||||
### CRIMSON is distributed in the hope that it will be useful, but
|
||||
### WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
### GNU General Public License for more details.
|
||||
###
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# `psql`
|
||||
# This is an alias for running
|
||||
# $ `compose` exec postgres psql -U $POSTGRES_USER $POSTGRES_DB.
|
||||
#
|
||||
# When working on a crimson project, you will likely have to lurk within the
|
||||
# database at times. This makes it nicer enter it since you dont have to type
|
||||
# the full command. :)
|
||||
|
||||
# ================================================================ CONSTANTS ==
|
||||
# ROOT: This is the folder the crimson project is located in. ROOT is used to
|
||||
# load the crimson environment. We need this since that is where POSTGRES_USER
|
||||
# and POSTGRES_DB are stored.
|
||||
# CALL_ROOT: This is the folder that the user who called `compose` is currently
|
||||
# in. For crimson to work this must be the folder that your project using
|
||||
# crimson is. This is because crimson loads `.env` here to load any user
|
||||
# specified environment. `.env` is needed in cause you override POSTGRES_USER
|
||||
# and/or POSTGRES_DB.
|
||||
ROOT="$(dirname "$0")"
|
||||
CALL_ROOT="$(pwd)"
|
||||
|
||||
# ================================================================ BOOTSTRAP ==
|
||||
# Load `base.env` and `.env`, then launch psql in docker.
|
||||
|
||||
source "$ROOT/base.env"
|
||||
source "$CALL_ROOT/.env"
|
||||
$ROOT/compose exec postgres psql -U "${POSTGRES_USER}" "${POSTGRES_DB}"
|
309
src/_base.php
Normal file
309
src/_base.php
Normal file
|
@ -0,0 +1,309 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Gives access to imporant
|
||||
* needed utility functions for
|
||||
* accessing everything else!
|
||||
*/
|
||||
abstract class Base {
|
||||
|
||||
// ======================================================== LOADABLE OBJECTS ==
|
||||
|
||||
// keep track of what has been loaded
|
||||
private static array $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';
|
||||
|
||||
// dont reload an ohject
|
||||
if (array_key_exists($path, self::$loaded))
|
||||
return self::$loaded[$path];
|
||||
|
||||
// only load a object if it exists
|
||||
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();
|
||||
self::$loaded[$path] = $obj;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a model
|
||||
* @param string $name - the name of the model to load
|
||||
*/
|
||||
public function load_model($name): Model|NULL
|
||||
{
|
||||
$dir = WEB_ROOT . '/_model';
|
||||
return $this->load_type($name, $dir, 'model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a controller
|
||||
* @param string $name - the name of the controller to load
|
||||
*/
|
||||
public function load_controller($name): Controller|NULL
|
||||
{
|
||||
$dir = WEB_ROOT . '/_controller';
|
||||
return $this->load_type($name, $dir, 'controller');
|
||||
}
|
||||
|
||||
// ==================================================================== LANG ==
|
||||
|
||||
// current loaded language strings
|
||||
private static array $loaded_lang = array();
|
||||
private static array $loaded_files = array();
|
||||
|
||||
/**
|
||||
* Loads a php lang file into the lang array
|
||||
*/
|
||||
private function load_lang_file(string $file): void
|
||||
{
|
||||
if (isset(self::$loaded[$file]))
|
||||
return;
|
||||
self::$loaded[$file] = TRUE;
|
||||
|
||||
$lang = self::$loaded_lang;
|
||||
require($file);
|
||||
self::$loaded_lang = $lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads each php file lang strings in a directory
|
||||
*/
|
||||
private function load_lang_dir(string $dir): void
|
||||
{
|
||||
if ($handle = opendir($dir)) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ($entry === '.' || $entry === '..')
|
||||
continue;
|
||||
$this->load_lang_file($entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given common lang
|
||||
*/
|
||||
public function load_lang(string ...$langs): array
|
||||
{
|
||||
$root = WEB_ROOT . '/lang';
|
||||
|
||||
foreach ($langs as $lang) {
|
||||
$file = "{$root}/{$lang}.php";
|
||||
$dir = "{$root}/{$lang}";
|
||||
|
||||
if (file_exists($file))
|
||||
$this->load_lang_file($file);
|
||||
else if (is_dir($dir))
|
||||
$this->load_lang_dir($dir);
|
||||
|
||||
}
|
||||
|
||||
return self::$loaded_lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently loaded lang
|
||||
*/
|
||||
public function get_lang(): array
|
||||
{
|
||||
return self::$loaded_lang;
|
||||
}
|
||||
|
||||
// ================================================================ DATABASE ==
|
||||
|
||||
// current database connection
|
||||
private static ?DatabaseHelper $db = NULL;
|
||||
|
||||
/**
|
||||
* Loads the database
|
||||
*/
|
||||
public function db(): DatabaseHelper
|
||||
{
|
||||
if (!self::$db)
|
||||
self::$db = new DatabaseHelper();
|
||||
return self::$db;
|
||||
}
|
||||
|
||||
// ================================================================ METADATA ==
|
||||
|
||||
/**
|
||||
* Gets the stamp for a asset path
|
||||
* @param string $path
|
||||
*/
|
||||
public function asset_stamp(string $path): int
|
||||
{
|
||||
if (ENVIRONMENT == 'devlopment')
|
||||
return time();
|
||||
if (isset(FILE_TIMES[$path]))
|
||||
return FILE_TIMES[$path];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a full path url from a relative path
|
||||
* @param string $path
|
||||
* @param bool $timestamp
|
||||
*/
|
||||
public function get_url(string $path, bool $timestamp = FALSE): string
|
||||
{
|
||||
$scheme = 'http';
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']))
|
||||
$scheme = $_SERVER['HTTP_X_FORWARDED_PROTO'];
|
||||
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
|
||||
if (ENVIRONMENT == 'production') {
|
||||
$default = CONFIG['domain'];
|
||||
$allowed = CONFIG['allowed_hosts'];
|
||||
if (!is_array($allowed))
|
||||
$allowed = [$allowed];
|
||||
if (!in_array($host, $allowed))
|
||||
$host = $default;
|
||||
}
|
||||
|
||||
$base = CONFIG['base_path'];
|
||||
$url = "{$scheme}://{$host}{$base}{$path}";
|
||||
if ($timestamp) {
|
||||
$time = $this->asset_stamp($path);
|
||||
$url .= "?timestamp={$time}";
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a js html link
|
||||
* @param string $path - the path to the js file
|
||||
*/
|
||||
public function link_js(string $path): string
|
||||
{
|
||||
$stamp = $this->asset_stamp("public/$path");
|
||||
$href = $this->get_url("public/{$path}?timestamp={$stamp}");
|
||||
return '<script src="'. $href .'"></script>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a css html link
|
||||
* @param string $path - the path to the css file
|
||||
*/
|
||||
public function link_css(string $path): string
|
||||
{
|
||||
$stamp = $this->asset_stamp("public/$path");
|
||||
$href = $this->get_url("public/{$path}?timestamp={$stamp}");
|
||||
return '<link rel="stylesheet" href="'. $href .'">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a css html link
|
||||
* @param string $path - the path to the css file
|
||||
*/
|
||||
public function embed_css(string $path): string
|
||||
{
|
||||
$file = PUBLIC_ROOT . '/' . $path;
|
||||
if (file_exists($file)) {
|
||||
$text = file_get_contents($file);
|
||||
return "<style>{$text}</style>";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================== HTTP POST ==
|
||||
|
||||
/**
|
||||
* Gets http POST data from $_POST if x-url-encoded, or parsed from
|
||||
* php://input if AJAX. Returns FALSE if input is not a post request,
|
||||
* or NULL if unable to parse request body.
|
||||
*/
|
||||
private function get_post_data()
|
||||
{
|
||||
static $data = NULL;
|
||||
|
||||
if (isset($data))
|
||||
return $data;
|
||||
|
||||
// not a POST request
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST')
|
||||
return NULL;
|
||||
|
||||
// ajax
|
||||
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
|
||||
$data = json_decode(file_get_contents("php://input"),
|
||||
true);
|
||||
// on failure, make sure $data is FALSE and not NULL
|
||||
if (!$data)
|
||||
$data = FALSE;
|
||||
return $data;
|
||||
}
|
||||
|
||||
// x-url-encoded or form-data
|
||||
$data = $_POST;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP POST information if POST request.
|
||||
* @returns $_POST if $key is not set and request is POST.
|
||||
* @returns value at $key if $key is set and request is POST.
|
||||
* @returns FALSE if value at $key is not set and request is POST.
|
||||
* @returns FALSE if request is POST but has invalid body.
|
||||
* @returns NULL if request is not POST.
|
||||
*/
|
||||
public function post_data(?string $key = NULL)
|
||||
{
|
||||
$data = $this->get_post_data();
|
||||
|
||||
if (!$data)
|
||||
return $data;
|
||||
|
||||
if (!$key)
|
||||
return $data;
|
||||
|
||||
if (!isset($data[$key]))
|
||||
return FALSE;
|
||||
|
||||
return $data[$key];
|
||||
}
|
||||
|
||||
}
|
66
src/_controller.php
Normal file
66
src/_controller.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
abstract class Controller extends Base {
|
||||
|
||||
// used by lib meta
|
||||
public array $CRIMSON_data = array();
|
||||
|
||||
/**
|
||||
* Default index for a app, empty
|
||||
*/
|
||||
public function index(): void {}
|
||||
|
||||
/**
|
||||
* Lodas a view
|
||||
*/
|
||||
protected function view(string $CRIMSON_name, array $data = array()): void
|
||||
{
|
||||
$CRIMSON_path = WEB_ROOT . '/_views/' . $CRIMSON_name . '.php';
|
||||
$this->CRIMSON_data = $data;
|
||||
|
||||
if (!is_file($CRIMSON_path)) {
|
||||
CRIMSON_ERROR("View '{$CRIMSON_name}' does not exist!");
|
||||
return;
|
||||
}
|
||||
|
||||
extract($this->CRIMSON_data);
|
||||
require($CRIMSON_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirectes to a link
|
||||
*/
|
||||
protected function redirect(string $link): never
|
||||
{
|
||||
header('Location: '. $link, true, 301);
|
||||
CRIMSON_DIE();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a erorr page with a given
|
||||
* error code
|
||||
*/
|
||||
protected function error(int $code): never
|
||||
{
|
||||
// does not return
|
||||
ROUTER->handle_error($code);
|
||||
}
|
||||
|
||||
}
|
63
src/_model.php
Normal file
63
src/_model.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
abstract class Model extends Base {
|
||||
|
||||
private function flatten_array($arr): array {
|
||||
$fn = fn($e) => is_array($e) ? $e : [$e];
|
||||
return array_merge(...array_map($fn, $arr));
|
||||
}
|
||||
|
||||
protected function get_data(): ?array
|
||||
{
|
||||
/* return barebones base data */
|
||||
$data = array();
|
||||
$data['css'] = array();
|
||||
$data['js'] = array();
|
||||
|
||||
$style = CONFIG['style'];
|
||||
$js = CONFIG['js'];
|
||||
|
||||
$app = NULL;
|
||||
try {
|
||||
// get the class object of the child class, i.e. Blog_model
|
||||
$cls = new ReflectionClass(get_class($this));
|
||||
// the name of the route is the name of the file without .php
|
||||
$path = $cls->getFileName();
|
||||
$app = pathinfo($path, PATHINFO_FILENAME);
|
||||
// sanity check
|
||||
assert(is_string($app));
|
||||
} catch (Exception $_e) {
|
||||
$app = CONTEXT['app'];
|
||||
}
|
||||
|
||||
$data['css'] = $this->flatten_array([
|
||||
$style[''] ?? [],
|
||||
$style[$app] ?? [],
|
||||
]);
|
||||
|
||||
$data['js'] = $this->flatten_array([
|
||||
$js[''] ?? [],
|
||||
$js[$app] ?? [],
|
||||
]);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
75
src/config.php
Normal file
75
src/config.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// ENVIRONMENT
|
||||
//
|
||||
// devlopment - do not cache any assets
|
||||
// - use http host provided by user
|
||||
//
|
||||
// production - use generated timestamps for each file
|
||||
// - hard code http host to 'domain' lang string
|
||||
//
|
||||
if (!defined('ENVIRONMENT')) {
|
||||
if (getenv('ENVIRONMENT') !== FALSE)
|
||||
define('ENVIRONMENT', getenv('ENVIRONMENT'));
|
||||
else
|
||||
define('ENVIRONMENT', 'devlopment');
|
||||
}
|
||||
|
||||
// CONFIG
|
||||
// config values needed across the website
|
||||
//
|
||||
// domain - the default domain for the website
|
||||
//
|
||||
// allowed_hosts - accepted domains to use for the website
|
||||
//
|
||||
// base_path - the base path the website is located at
|
||||
//
|
||||
// theme_color - html hex color used for browser metadata
|
||||
//
|
||||
// routes - array of regex keys that match the request path and
|
||||
// - place it with the value if it matches
|
||||
// - e.g. '' => 'home' sends / to /home
|
||||
//
|
||||
// style - single or list of css styles to load on specific routes
|
||||
//
|
||||
// js - single or list of js script to load on specific routes
|
||||
//
|
||||
// autoload - list of directories to autoload all PHP files in them
|
||||
//
|
||||
define('BASE_CONFIG', array(
|
||||
/* core settings */
|
||||
'domain' => 'localhost',
|
||||
'allowed_hosts' => ['localhost'],
|
||||
'base_path' => '/',
|
||||
'theme_color' => '#181818',
|
||||
/* route overides */
|
||||
'routes' => array(),
|
||||
/* css to load on each route */
|
||||
'style' => array(),
|
||||
/* js to load on each route */
|
||||
'js' => array(),
|
||||
/* directories to autoload php code */
|
||||
'autoload' => array(),
|
||||
));
|
||||
|
||||
if (!defined('SITE_CONFIG'))
|
||||
define('SITE_CONFIG', array());
|
||||
|
||||
define('CONFIG', array_merge(BASE_CONFIG, SITE_CONFIG));
|
68
src/index.php
Normal file
68
src/index.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// ============================================================= ENVIRONMENT ==
|
||||
|
||||
// define folder paths
|
||||
define('CRIMSON_ROOT', '/opt/crimson');
|
||||
define('PHP_ROOT', '/opt/site');
|
||||
define('WEB_ROOT', PHP_ROOT . '/web');
|
||||
define('PUBLIC_ROOT', PHP_ROOT . '/public');
|
||||
|
||||
// =============================================================== BOOTSTRAP ==
|
||||
|
||||
// load the config
|
||||
@include(WEB_ROOT . '/config.php');
|
||||
require(CRIMSON_ROOT . '/config.php');
|
||||
|
||||
// load all core files (order matters)
|
||||
require(CRIMSON_ROOT . '/lib/database.php');
|
||||
require(CRIMSON_ROOT . '/_base.php');
|
||||
require(CRIMSON_ROOT . '/_model.php');
|
||||
require(CRIMSON_ROOT . '/_controller.php');
|
||||
require(CRIMSON_ROOT . '/router.php');
|
||||
require(CRIMSON_ROOT . '/lib/error.php');
|
||||
require(CRIMSON_ROOT . '/lib/hooks.php');
|
||||
require(CRIMSON_ROOT . '/lib/lang.php');
|
||||
require(CRIMSON_ROOT . '/lib/meta.php');
|
||||
require(CRIMSON_ROOT . '/lib/html.php');
|
||||
|
||||
// autoload requested directories
|
||||
foreach (CONFIG['autoload'] as $dir)
|
||||
foreach (glob(WEB_ROOT . $dir . '/*.php') as $file)
|
||||
require($file);
|
||||
|
||||
// load file stamps on production
|
||||
if (ENVIRONMENT == 'production')
|
||||
require('/var/run/crimson/stamp.php');
|
||||
|
||||
// =================================================================== START ==
|
||||
|
||||
try {
|
||||
CRIMSON_HOOK('init');
|
||||
(new Router())->handle_req();
|
||||
} catch (Error $e) {
|
||||
CRIMSON_error_handler(
|
||||
CRIMSON_E_FATAL_ERROR,
|
||||
$e->getMessage(),
|
||||
$e->getFile(),
|
||||
$e->getLine(),
|
||||
$e->getTrace(),
|
||||
);
|
||||
}
|
337
src/lib/database.php
Normal file
337
src/lib/database.php
Normal file
|
@ -0,0 +1,337 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
function __nullify(mixed $val): mixed
|
||||
{
|
||||
if (!$val) {
|
||||
return NULL;
|
||||
} else {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseQuery {
|
||||
|
||||
private \PDO $conn;
|
||||
private string $query;
|
||||
|
||||
private bool $where;
|
||||
private bool $set;
|
||||
|
||||
private array $param;
|
||||
|
||||
function __construct(\PDO $conn)
|
||||
{
|
||||
$this->conn = $conn;
|
||||
$this->query = '';
|
||||
|
||||
$this->set = FALSE;
|
||||
$this->where = FALSE;
|
||||
$this->param = array();
|
||||
}
|
||||
|
||||
///
|
||||
/// ARBITRARY QUERY
|
||||
///
|
||||
|
||||
public function query(string $query): DatabaseQuery
|
||||
{
|
||||
$this->query .= $query;
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// SELECT
|
||||
///
|
||||
|
||||
public function select(string $select): DatabaseQuery
|
||||
{
|
||||
$this->query .= "SELECT $select\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function from(string $from): DatabaseQuery
|
||||
{
|
||||
$this->query .= "FROM $from\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// INSERT
|
||||
///
|
||||
|
||||
public function insert_into(string $insert, string ...$columns): DatabaseQuery
|
||||
{
|
||||
$this->query .= "INSERT INTO $insert\n (";
|
||||
foreach ($columns as $idx => $column) {
|
||||
if ($idx !== 0) {
|
||||
$this->query .= ",";
|
||||
}
|
||||
$this->query .= $column;
|
||||
}
|
||||
$this->query .= ")\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function values(mixed ...$values): DatabaseQuery
|
||||
{
|
||||
$this->query .= "VALUES (";
|
||||
foreach ($values as $idx => $value) {
|
||||
if ($idx !== 0) {
|
||||
$this->query .= ",";
|
||||
}
|
||||
$this->query .= "?";
|
||||
array_push($this->param, $value);
|
||||
}
|
||||
$this->query .= ")\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// WHERE
|
||||
///
|
||||
|
||||
public function where(string $cond): DatabaseQuery
|
||||
{
|
||||
if (!$this->where) {
|
||||
$this->where = TRUE;
|
||||
$this->query .= "WHERE ";
|
||||
} else {
|
||||
$this->query .= "AND ";
|
||||
}
|
||||
$this->query .= "$cond ";
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $array
|
||||
*/
|
||||
public function where_in(string $column, array $array): DatabaseQuery
|
||||
{
|
||||
if (!$this->where) {
|
||||
$this->where = TRUE;
|
||||
$this->query .= "WHERE ";
|
||||
} else {
|
||||
$this->query .= "AND ";
|
||||
}
|
||||
if (empty($array)) {
|
||||
$this->query .= "FALSE\n";
|
||||
return $this;
|
||||
}
|
||||
$in = $this->in($array);
|
||||
$this->query .= "$column $in\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function in(array $array): string
|
||||
{
|
||||
$in = 'IN (';
|
||||
foreach ($array as $idx => $item) {
|
||||
if ($idx != 0) {
|
||||
$in .= ",";
|
||||
}
|
||||
$in .= "?";
|
||||
array_push($this->param, $item);
|
||||
}
|
||||
$in .= ")";
|
||||
return $in;
|
||||
}
|
||||
|
||||
///
|
||||
/// OPERATORS
|
||||
///
|
||||
|
||||
public function like(mixed $item): DatabaseQuery
|
||||
{
|
||||
$this->query .= "LIKE ?\n";
|
||||
array_push($this->param, $item);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function eq(mixed $item): DatabaseQuery
|
||||
{
|
||||
$this->query .= "= ?\n";
|
||||
array_push($this->param, $item);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ne(mixed $item): DatabaseQuery
|
||||
{
|
||||
$this->query .= "<> ?\n";
|
||||
array_push($this->param, $item);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function lt(mixed $item): DatabaseQuery
|
||||
{
|
||||
$this->query .= "< ?\n";
|
||||
array_push($this->param, $item);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function le(mixed $item): DatabaseQuery
|
||||
{
|
||||
$this->query .= "<= ?\n";
|
||||
array_push($this->param, $item);
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// JOINS
|
||||
///
|
||||
|
||||
public function join(string $table, string $on, string $type = 'LEFT'): DatabaseQuery
|
||||
{
|
||||
$this->query .= "$type JOIN $table ON $on\n";
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// LIMIT, OFFSET, ORDER
|
||||
///
|
||||
|
||||
public function limit(int $limit): DatabaseQuery
|
||||
{
|
||||
$this->query .= "LIMIT ?\n";
|
||||
array_push($this->param, $limit);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function offset(int $offset): DatabaseQuery
|
||||
{
|
||||
$this->query .= "OFFSET ?\n";
|
||||
array_push($this->param, $offset);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function order_by(string $column, string $order = 'ASC'): DatabaseQuery
|
||||
{
|
||||
$this->query .= "ORDER BY " . $column . ' ' . $order . ' ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
///
|
||||
/// COLLECT
|
||||
///
|
||||
|
||||
public function rows(mixed ...$params): array
|
||||
{
|
||||
$args = $this->param;
|
||||
foreach ($params as $param) {
|
||||
array_push($args, $param);
|
||||
}
|
||||
$stmt = $this->conn->prepare($this->query);
|
||||
try {
|
||||
$stmt->execute($args);
|
||||
} catch (Exception $ex) {
|
||||
echo $ex;
|
||||
echo '<br> >> caused by <<<br>';
|
||||
echo str_replace("\n", "<br>", $this->query);
|
||||
}
|
||||
return __nullify($stmt->fetchAll(PDO::FETCH_ASSOC)) ?? [];
|
||||
}
|
||||
|
||||
public function row(mixed ...$params): ?array
|
||||
{
|
||||
$args = $this->param;
|
||||
foreach ($params as $param) {
|
||||
array_push($args, $param);
|
||||
}
|
||||
$stmt = $this->conn->prepare($this->query);
|
||||
$stmt->execute($args);
|
||||
return __nullify($stmt->fetch(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function execute(mixed ...$params): bool
|
||||
{
|
||||
$args = $this->param;
|
||||
foreach ($params as $param) {
|
||||
array_push($args, $param);
|
||||
}
|
||||
$stmt = $this->conn->prepare($this->query);
|
||||
try {
|
||||
$stmt->execute($args);
|
||||
return TRUE;
|
||||
} catch (Exception $_e) {
|
||||
echo $_e;
|
||||
echo '<br> >> caused by <<<br>';
|
||||
echo str_replace("\n", "<br>", $this->query);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DatabaseHelper
|
||||
* allows queries on the
|
||||
* postgres database
|
||||
*/
|
||||
class DatabaseHelper {
|
||||
|
||||
private ?\PDO $conn;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->conn = NULL;
|
||||
}
|
||||
|
||||
private function connect(): \PDO
|
||||
{
|
||||
if ($this->conn === NULL) {
|
||||
$user = getenv("POSTGRES_USER");
|
||||
$pass = getenv("POSTGRES_PASSWORD");
|
||||
$db = getenv("POSTGRES_DB");
|
||||
$host = 'postgres';
|
||||
$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(string $select): DatabaseQuery
|
||||
{
|
||||
$conn = $this->connect();
|
||||
$query = new DatabaseQuery($conn);
|
||||
return $query->select($select);
|
||||
}
|
||||
|
||||
public function insert_into(string $insert, string ...$columns): DatabaseQuery
|
||||
{
|
||||
$conn = $this->connect();
|
||||
$query = new DatabaseQuery($conn);
|
||||
return $query->insert_into($insert, ...$columns);
|
||||
}
|
||||
|
||||
public function query(string $query_str): DatabaseQuery
|
||||
{
|
||||
$conn = $this->connect();
|
||||
$query = new DatabaseQuery($conn);
|
||||
return $query->query($query_str);
|
||||
}
|
||||
}
|
||||
|
218
src/lib/error.php
Normal file
218
src/lib/error.php
Normal file
|
@ -0,0 +1,218 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* CRIMSON ERROR FUNCTIONS
|
||||
*
|
||||
* Insead of using trigger_error, crimson has its own CRIMSON_<type> error
|
||||
* functions. It is prefered to use this they hook more nicely into crimson.
|
||||
* trigger_error is still okay to use since crimson creates its own error
|
||||
* handler, and will catch it anyways.
|
||||
*
|
||||
* WARNING: DO NOT create your own error handler since this MAY interfere with
|
||||
* crimson's control flow.
|
||||
*/
|
||||
|
||||
// crimson's fatal error handler will set $errno to CRIMSON_E_FATAL_ERROR.
|
||||
define('CRIMSON_E_FATAL_ERROR', 0);
|
||||
|
||||
function __CRIMSON_error_pretty_print_name(int $errno) {
|
||||
switch ($errno) {
|
||||
case CRIMSON_E_FATAL_ERROR: // 0 //
|
||||
return 'Fatal Error';
|
||||
|
||||
case E_ERROR: // 1 //
|
||||
case E_CORE_ERROR: // 16 //
|
||||
case E_USER_ERROR: // 256 //
|
||||
case E_RECOVERABLE_ERROR: // 4096 //
|
||||
return 'Error';
|
||||
|
||||
case E_WARNING: // 2 //
|
||||
case E_CORE_WARNING: // 32 //
|
||||
case E_USER_WARNING: // 512 //
|
||||
return 'Warning';
|
||||
|
||||
case E_PARSE: // 4 //
|
||||
return 'Parse Error';
|
||||
|
||||
case E_NOTICE: // 8 //
|
||||
case E_USER_NOTICE: // 1024 //
|
||||
return 'Notice';
|
||||
|
||||
case E_COMPILE_ERROR: // 128 //
|
||||
return 'Compile Error';
|
||||
|
||||
case E_DEPRECATED: // 8192 //
|
||||
case E_USER_DEPRECATED: // 16384 //
|
||||
return 'Deprecated';
|
||||
|
||||
default:
|
||||
return 'Unknown Error';
|
||||
}
|
||||
}
|
||||
|
||||
function __CRIMSON_error_pretty_print_bg_color(int $errno) {
|
||||
switch ($errno) {
|
||||
case CRIMSON_E_FATAL_ERROR: // 0 //
|
||||
case E_ERROR: // 1 //
|
||||
case E_PARSE: // 4 //
|
||||
case E_CORE_ERROR: // 16 //
|
||||
case E_COMPILE_ERROR: // 128 //
|
||||
case E_USER_ERROR: // 256 //
|
||||
case E_RECOVERABLE_ERROR: // 4096 //
|
||||
return '#dc143c';
|
||||
|
||||
case E_WARNING: // 2 //
|
||||
case E_CORE_WARNING: // 32 //
|
||||
case E_USER_WARNING: // 512 //
|
||||
case E_DEPRECATED: // 8192 //
|
||||
case E_USER_DEPRECATED: // 16384 //
|
||||
return '#db6d13';
|
||||
|
||||
case E_NOTICE: // 8 //
|
||||
case E_USER_NOTICE: // 1024 //
|
||||
default:
|
||||
return '#13a6db';
|
||||
}
|
||||
}
|
||||
|
||||
function __CRIMSON_error_pretty_print_text_color(int $errno) {
|
||||
switch ($errno) {
|
||||
case CRIMSON_E_FATAL_ERROR: // 0 //
|
||||
case E_ERROR: // 1 //
|
||||
case E_PARSE: // 4 //
|
||||
case E_CORE_ERROR: // 16 //
|
||||
case E_COMPILE_ERROR: // 128 //
|
||||
case E_USER_ERROR: // 256 //
|
||||
case E_RECOVERABLE_ERROR: // 4096 //
|
||||
return '#fff';
|
||||
|
||||
case E_WARNING: // 2 //
|
||||
case E_CORE_WARNING: // 32 //
|
||||
case E_USER_WARNING: // 512 //
|
||||
case E_DEPRECATED: // 8192 //
|
||||
case E_USER_DEPRECATED: // 16384 //
|
||||
return '#fff';
|
||||
|
||||
case E_NOTICE: // 8 //
|
||||
case E_USER_NOTICE: // 1024 //
|
||||
default:
|
||||
return '#181818';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* __CRIMSON_error_pretty_print
|
||||
*
|
||||
* Prints a pretty HTML error message using the same set of parameters
|
||||
* from PHP's error handler.
|
||||
*
|
||||
* Unless CRIMSON detects that you are using a tty, crimson will opt to
|
||||
* pretty print all errors.
|
||||
*/
|
||||
function __CRIMSON_error_pretty_print(
|
||||
int $errno,
|
||||
string $errstr,
|
||||
?string $errfile = NULL,
|
||||
?int $errline = NULL,
|
||||
?array $errcontext = NULL,
|
||||
): void {
|
||||
|
||||
$name = __CRIMSON_error_pretty_print_name($errno);
|
||||
$bg = __CRIMSON_error_pretty_print_bg_color($errno);
|
||||
$text = __CRIMSON_error_pretty_print_text_color($errno);
|
||||
|
||||
$root_style = "
|
||||
all: unset !important;
|
||||
display: block !important;
|
||||
margin: .1em !important;
|
||||
background: {$bg} !important;
|
||||
color: {$text} !important;
|
||||
font-family: Helvetica, Verdana, Courier, monospace !important;
|
||||
font-size: 14px !important;";
|
||||
|
||||
$div_style = "
|
||||
padding: .5em; !important;";
|
||||
|
||||
$span_style = "
|
||||
display: block !important";
|
||||
|
||||
$title_style="
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold !important;
|
||||
background: rgba(255,255,255,.2); !important";
|
||||
|
||||
$html = <<<EOF
|
||||
<div style="{$root_style}">
|
||||
<div style="{$div_style}\n{$title_style}">
|
||||
(!) {$name}
|
||||
</div>
|
||||
<div style="{$div_style}">
|
||||
<span style="{$span_style}">
|
||||
In file {$errfile}:{$errline}
|
||||
</span>
|
||||
<span style="{$span_style}">
|
||||
>>> {$errstr}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
EOF;
|
||||
echo $html;
|
||||
}
|
||||
|
||||
function __CRIMSON_error_print(
|
||||
int $errno,
|
||||
string $errstr,
|
||||
?string $errfile = NULL,
|
||||
?int $errline = NULL,
|
||||
?array $errcontext = NULL,
|
||||
): void {
|
||||
$name = __CRIMSON_error_pretty_print_name($errno);
|
||||
echo "{$name}: {$errstr}\n\tIn file {$errfile}:{$errline}\n";
|
||||
}
|
||||
|
||||
function CRIMSON_error_handler(
|
||||
int $errno,
|
||||
string $errstr,
|
||||
?string $errfile = NULL,
|
||||
?int $errline = NULL,
|
||||
?array $errcontext = NULL,
|
||||
): void {
|
||||
__CRIMSON_error_pretty_print($errno, $errstr, $errfile, $errline, $errcontext);
|
||||
}
|
||||
|
||||
set_error_handler("CRIMSON_error_handler");
|
||||
|
||||
function CRIMSON_ERROR(string $msg): void {
|
||||
$frame = debug_backtrace()[1];
|
||||
CRIMSON_error_handler(E_ERROR, $msg,
|
||||
$frame['file'], $frame['line']);
|
||||
}
|
||||
|
||||
function CRIMSON_FATAL_ERROR(string $msg): void {
|
||||
$frame = debug_backtrace()[1];
|
||||
CRIMSON_error_handler(CRIMSON_E_FATAL_ERROR, $msg,
|
||||
$frame['file'], $frame['line']);
|
||||
}
|
||||
|
||||
function CRIMSON_WARNING(string $msg): void {
|
||||
$frame = debug_backtrace()[1];
|
||||
CRIMSON_error_handler(E_WARNING, $msg,
|
||||
$frame['file'], $frame['line']);
|
||||
}
|
178
src/lib/hooks.php
Normal file
178
src/lib/hooks.php
Normal file
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* CRIMSON HOOKS
|
||||
*
|
||||
* Crimson supports hooks to allow the user to add functionality inside
|
||||
* crimson's control flow. This can be used for handling errors, or having
|
||||
* custom routing functionality.
|
||||
*
|
||||
* The current supported hooks are: 'error', 'pre_route', 'init', and 'die'.
|
||||
*
|
||||
* To run a hook yourself you can call CRIMSON_HOOK($name, [$args]);
|
||||
*
|
||||
* To create a handler, make a function in the global scope called:
|
||||
* CRIMSON_<name>_hook. i.e. if you are making a init hook handler, make a
|
||||
* function called CRIMSON_init_hook(...).
|
||||
*
|
||||
* If a hook is called and a user hook handeler does not exist, the crimson
|
||||
* builtin hook will be run instead.
|
||||
*
|
||||
* NOTE: It is also allowed for you to create your own hook types, but you must
|
||||
* also create your own handler for it. If you dont, it will result in a error
|
||||
* since crimson will not have a builtin hook handler to fall back to.
|
||||
*
|
||||
* NOTE: CRIMSON_HOOK does support a third argument called $builtin, but this
|
||||
* is only to be used by crimson itself, so please dont use it.
|
||||
*
|
||||
* WARNING: If a hook cannot find a handler to run, it will call
|
||||
* CRIMSON_FATAL_ERROR and NEVER return.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CRIMSON builtin ERROR hook.
|
||||
*
|
||||
* $req - The current request parsed by the Router. All fields are gurenteed
|
||||
* to be set BESIDES 'uri' and 'uri_str', since parsing of the URI can fail
|
||||
* and result to a call to this error handler.
|
||||
*
|
||||
* This hook is run when any Controller or the Router runs into an issue and
|
||||
* fails with an HTTP Error.
|
||||
*
|
||||
* EXAMPLE: If the Router cannot find the requested Controller and Method
|
||||
* based in the URI path, it will raise 404 Not Found.
|
||||
*
|
||||
* EXAMPLE: If the crimson postgres database is enabled, and db-init either
|
||||
* failed or is still running (the database is not ready), crimson will raise
|
||||
* 503 Service Unavaliable.
|
||||
*
|
||||
* EXAMPLE: If the provided URI to PHP is unable to be parsed, crimson will
|
||||
* raise 400 Bad Request.
|
||||
*
|
||||
* This is hook is called in ROUTER->handle_error(int code) and
|
||||
* Controller->error(int code).
|
||||
*
|
||||
* NOTE: Unlike CRIMSON's DIE hook, it is supported to re-enter crimson code.
|
||||
* This error handler must never return, but it is okay to recall crimson to
|
||||
* try to print an error page.
|
||||
*
|
||||
* WARNING: If the user tries to re-enter crimson from this handler and ends
|
||||
* up raising another error, the user error handler WILL NOT be called again.
|
||||
* To prevent recursion, a user error handle WILL only ever be called ONCE.
|
||||
* This is due to that the handler has return type 'never', and is also not
|
||||
* allowed to recurse.
|
||||
*/
|
||||
function CRIMSON_builtin_error_hook(array $req, int $code): never
|
||||
{
|
||||
http_response_code($code);
|
||||
CRIMSON_DIE("{$code} {$req['uri_str']}");
|
||||
}
|
||||
|
||||
/**
|
||||
* CRIMSON builtin PRE ROUTE hook.
|
||||
*
|
||||
* This hook does nothing by default since all required CRIMSON routing is done
|
||||
* in router.php.
|
||||
*/
|
||||
function CRIMSON_builtin_pre_route_hook(Router $router): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* CRIMSON builtin INIT hook.
|
||||
*
|
||||
* This hook does nothing by default since all required CRIMSON init work is
|
||||
* run in index.php.
|
||||
*
|
||||
* This hook can be overridden to run any user required initalization code
|
||||
* before CRIMSON launches and runs the router.
|
||||
*
|
||||
* WARNING: The ROUTER is NOT YET created when this hook is run. Do not call
|
||||
* ROUTER or ANY CRIMSON code in a user specified init hook. Doing so is
|
||||
* UNDEFINED BEHAVIOR.
|
||||
*/
|
||||
function CRIMSON_builtin_init_hook(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* CRIMSON builtin DIE hook.
|
||||
*
|
||||
* Calls die with $status, i.e. this is an alias for die().
|
||||
*/
|
||||
function CRIMSON_builtin_die_hook(string|int $status = 0): never
|
||||
{
|
||||
die($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a hook for crimson.
|
||||
*
|
||||
* $name - the name of the hook
|
||||
* $args - the arguments to pass to the hook
|
||||
* $builtin - force the use of the builtin hook
|
||||
*
|
||||
* Looks for CRIMSON_$name_hook first, which is the custom user defined hook.
|
||||
* If it does not exist, it will run CRIMSON_builtin_$name_hook instead.
|
||||
*/
|
||||
function CRIMSON_HOOK(
|
||||
string $name,
|
||||
array $args = array(),
|
||||
bool $builtin = FALSE,
|
||||
): void {
|
||||
$names = array();
|
||||
if (!$builtin)
|
||||
$names[] = "CRIMSON_{$name}_hook";
|
||||
$names[] = "CRIMSON_builtin_{$name}_hook";
|
||||
|
||||
// find handler
|
||||
$handler = NULL;
|
||||
foreach ($names as $name) {
|
||||
try {
|
||||
$handler = new ReflectionFunction($name);
|
||||
if ($handler)
|
||||
break;
|
||||
} catch (Exception $_e) {};
|
||||
}
|
||||
|
||||
// error in invalid hook
|
||||
if (!$handler) {
|
||||
CRIMSON_ERROR("Invalid hook: {$name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// i dont care to argument check,
|
||||
// if someone screws up a hook we have bigger problems
|
||||
$handler->invoke(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes die() in php. But allows the user to add a hook to handle any
|
||||
* loose resources before php kills itself.
|
||||
*
|
||||
* NOTE: A DIE hook should NEVER be handeled and return. A user provided hook
|
||||
* must also have a return type of never and immediately die. Do NOT
|
||||
* call any crimson code such as rerunning the ROUTER. Doing so will result in
|
||||
* UNDEFINED BEHAVIOR and nasal demons showing up at your doorstep!!!
|
||||
*/
|
||||
function CRIMSON_DIE(string|int $status = 0): never
|
||||
{
|
||||
CRIMSON_HOOK('die', [$status]);
|
||||
}
|
95
src/lib/html.php
Normal file
95
src/lib/html.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
function esc(string $data, bool $string_esc = FALSE): string {
|
||||
$flags = ENT_SUBSTITUTE | ENT_HTML401;
|
||||
if ($string_esc)
|
||||
$flags |= ENT_QUOTES;
|
||||
return htmlspecialchars($data, $flags);
|
||||
}
|
||||
|
||||
function status_code_msg(int $code): ?string {
|
||||
|
||||
static $status_code = array(
|
||||
100 => "Continue",
|
||||
101 => "Switching Protocols",
|
||||
200 => "OK",
|
||||
201 => "Created",
|
||||
202 => "Accepted",
|
||||
203 => "Non-Authoritative Information",
|
||||
204 => "No Content",
|
||||
205 => "Reset Content",
|
||||
206 => "Partial Content",
|
||||
300 => "Multiple Choices",
|
||||
301 => "Moved Permanently",
|
||||
302 => "Found",
|
||||
303 => "See Other",
|
||||
304 => "Not Modified",
|
||||
//305 => "Use Proxy",
|
||||
//306 => "unused",
|
||||
307 => "Temporary Redirect",
|
||||
308 => "Permanent Redirect",
|
||||
400 => "Bad Request",
|
||||
401 => "Unauthorized",
|
||||
402 => "Payment Required",
|
||||
403 => "Forbidden",
|
||||
404 => "Not Found",
|
||||
405 => "Method Not Allowed",
|
||||
406 => "Not Acceptable",
|
||||
407 => "Proxy Authentication Required",
|
||||
408 => "Request Timeout",
|
||||
409 => "Conflict",
|
||||
410 => "Gone",
|
||||
411 => "Length Required",
|
||||
412 => "Precondition Failed",
|
||||
413 => "Content Too Large",
|
||||
414 => "URI Too Long",
|
||||
415 => "Unsupported Media Type",
|
||||
416 => "Range Not Satisfiable",
|
||||
417 => "Expectation Failed",
|
||||
418 => "I'm a teapot",
|
||||
421 => "Misdirected Request",
|
||||
422 => "Unprocessable Content",
|
||||
423 => "Locked",
|
||||
424 => "Failed Dependency",
|
||||
425 => "Too Early",
|
||||
426 => "Upgrade Required",
|
||||
428 => "Precondition Required",
|
||||
429 => "Too Many Requests",
|
||||
431 => "Request Header Fields Too Large",
|
||||
451 => "Unavailable For Legal Reasons",
|
||||
500 => "Internal Server Error",
|
||||
501 => "Not Implemented",
|
||||
502 => "Bad Gateway",
|
||||
503 => "Service Unavailable",
|
||||
504 => "Gateway Timeout",
|
||||
505 => "HTTP Version Not Supported",
|
||||
506 => "Variant Also Negotiates",
|
||||
507 => "Insufficient Storage",
|
||||
508 => "Loop Detected",
|
||||
510 => "Not Extended",
|
||||
511 => "Network Authentication Required",
|
||||
);
|
||||
|
||||
return $status_code[$code] ?? NULL;
|
||||
}
|
||||
|
||||
function is_valid_status_code(int $code): bool {
|
||||
return is_string(status_code_msg($code));
|
||||
}
|
186
src/lib/lang.php
Normal file
186
src/lib/lang.php
Normal file
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Returns the lang string for the provided $key
|
||||
*
|
||||
* $key - The key of the lang string
|
||||
* $default - The string to use if lang string $key is undefined
|
||||
* $sub - List of values to substitute using php's sprinf
|
||||
*/
|
||||
function lang(
|
||||
string $key,
|
||||
?string $default = NULL,
|
||||
?array $sub = NULL
|
||||
): string {
|
||||
$lang = ROUTER->get_lang();
|
||||
$result = NULL;
|
||||
|
||||
// lookup lang key
|
||||
if (isset($lang[$key]))
|
||||
$result = $lang[$key];
|
||||
|
||||
// replace with $default if undefined
|
||||
if ($result === NULL && $default !== NULL)
|
||||
$result = $default;
|
||||
|
||||
// error if undefined
|
||||
if ($result === NULL) {
|
||||
CRIMSON_WARNING('Undefined lang string: ' . $key);
|
||||
return $key;
|
||||
}
|
||||
|
||||
// make substitutions
|
||||
if ($sub) {
|
||||
if (!is_array($sub))
|
||||
$sub = [$sub];
|
||||
$result = sprintf($result, ...$sub);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a html element (button, a, div, ...) containing content from
|
||||
* the lang string, icon, aria tags, and/or tooltips. Text content and icon
|
||||
* are contained in a seconed inner html element (span, h1, ...).
|
||||
*
|
||||
* ilang has up to four parts: text, tooltip, icon class, and icon content.
|
||||
* Each part is loaded from a different lang string using $key as the prefix.
|
||||
*
|
||||
* == LANG_KEYS ==
|
||||
*
|
||||
* NAME | LANG KEY | REQUIRED | DESCRIPTION
|
||||
* --------------------------------------------------------------------------
|
||||
* text | $key_text | yes | The text content of the element. Text
|
||||
* | | | content will always uppercase the first
|
||||
* | | | letter.
|
||||
* tip | $key_tip | no | The tool tip of the element.
|
||||
* icon | $key_icon | no | Adds a <i> element with the class
|
||||
* | | | <lang string>.
|
||||
* content | $key_content | no | If icon, adds <lang string> as the
|
||||
* | | | inner html of the icon.
|
||||
*
|
||||
* == ARGUMENTS ==
|
||||
*
|
||||
* NAME | REQUIRED | DEFAULT | DESCRIPTION
|
||||
* ---------------------------------------------------------------------------
|
||||
* $key | yes | | The key of the interface lang string.
|
||||
* $class | no | | The class of the html element.
|
||||
* $id | no | | The id of the html element.
|
||||
* $sub | no | [] | Substitution arguments passed into lang()
|
||||
* | | | in both $key_text and $key_tip.
|
||||
* $type | no | 'a' | Sets the type of the html element.
|
||||
* $subtype | no | 'span' | Sets the type of the inner html element.
|
||||
* $attrs | no | array() | Sets html attributes using the key/value
|
||||
* | | | pairs from $attrs. $class, $id, $href, and
|
||||
* | | | and $onclick are all short hand for
|
||||
* | | | $attrs['<name>']; Named attr arguments take
|
||||
* | | | priority over any defined in $attrs.
|
||||
* $style | no | | $attrs['style'] = $style.
|
||||
* $href | no | | $attrs['href'] = $href. $type = 'a';
|
||||
* $onclick | no | | $attrs['onclick'] = $onclick. $type = 'button'.
|
||||
*
|
||||
* NOTE: For any non required argument that is falsy, it is converted back to
|
||||
* its default value.
|
||||
*
|
||||
* NOTE: For any non required argument that does not have a default value
|
||||
* listed, falsy values turn off that attribute or functionality.
|
||||
*
|
||||
* WARNING: $href and $onclick also modify the default $type. If $type is
|
||||
* passed to ilang, that type will be used instead.
|
||||
*
|
||||
* WARNING: Lang strings WILL be html escaped along with each atribute value.
|
||||
* Everything else will not be sanitized by this function.
|
||||
*/
|
||||
function ilang(
|
||||
string $key,
|
||||
?string $class = NULL,
|
||||
?string $id = NULL,
|
||||
array $sub = [],
|
||||
?string $type = NULL,
|
||||
string $subtype = 'span',
|
||||
array $attrs = array(),
|
||||
?string $style = NULL,
|
||||
?string $href = NULL,
|
||||
?string $onclick = NULL,
|
||||
): string {
|
||||
// read lang keys
|
||||
$text = lang("{$key}_text", sub: $sub);
|
||||
$tip = lang("{$key}_tip", '', sub: $sub);
|
||||
$icon = lang("{$key}_icon", '');
|
||||
$content = lang("{$key}_content", '');
|
||||
|
||||
// uppercase
|
||||
$text = ucfirst($text);
|
||||
|
||||
// set $type if falsy
|
||||
if (!$type) {
|
||||
if ($href)
|
||||
$type = 'a';
|
||||
else if ($onclick)
|
||||
$type = 'button';
|
||||
else
|
||||
$type = 'a';
|
||||
}
|
||||
|
||||
// populate $attrs with named arguments
|
||||
if ($tip) {
|
||||
$attrs['title'] = $tip;
|
||||
$attrs['aria-label'] = $tip;
|
||||
}
|
||||
if ($class)
|
||||
$attrs['class'] = "{$class} ilang";
|
||||
else
|
||||
$attrs['class'] = "ilang";
|
||||
if ($id)
|
||||
$attrs['id'] = $id;
|
||||
if ($style)
|
||||
$attrs['style'] = $style;
|
||||
if ($href)
|
||||
$attrs['href'] = $href;
|
||||
if ($onclick)
|
||||
$attrs['onclick'] = $onclick;
|
||||
|
||||
$html = "";
|
||||
// open tag
|
||||
$html .= "<{$type}";
|
||||
foreach ($attrs as $key => $value) {
|
||||
$value = esc($value, TRUE); // html tag & string escape
|
||||
$html .= " {$key}=\"{$value}\"";
|
||||
}
|
||||
$html .= ">";
|
||||
// icon
|
||||
if ($icon) {
|
||||
$icon = esc($icon, TRUE); // html tag & string escape
|
||||
$html .= "<i class=\"{$icon}\">";
|
||||
if ($content) {
|
||||
$content = esc($content); // html tag escape
|
||||
$html .= "{$content}";
|
||||
}
|
||||
$html .= "</i>";
|
||||
}
|
||||
// content
|
||||
$text = esc($text); // html tag escape
|
||||
$html .= "<{$subtype}>{$text}</{$subtype}>";
|
||||
// close tag
|
||||
$html .= "</{$type}>";
|
||||
|
||||
return $html;
|
||||
}
|
28
src/lib/meta.php
Normal file
28
src/lib/meta.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
function CRIMSON_META(Controller $self): string {
|
||||
$data = $self->CRIMSON_data;
|
||||
$html = '';
|
||||
foreach ($data['css'] as $css)
|
||||
$html .= $self->link_css($css);
|
||||
foreach ($data['js'] as $js)
|
||||
$html .= $self->link_js($js);
|
||||
return $html;
|
||||
}
|
236
src/router.php
Normal file
236
src/router.php
Normal file
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
/// CRIMSON --- A simple PHP framework.
|
||||
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
|
||||
///
|
||||
/// This file is part of CRIMSON.
|
||||
///
|
||||
/// CRIMSON is free software; you can redistribute it and/or modify it
|
||||
/// under the terms of the GNU General Public License as published by
|
||||
/// the Free Software Foundation; either version 3 of the License, or (at
|
||||
/// your option) any later version.
|
||||
///
|
||||
/// CRIMSON is distributed in the hope that it will be useful, but
|
||||
/// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
/// GNU General Public License for more details.
|
||||
///
|
||||
/// You should have received a copy of the GNU General Public License
|
||||
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class Router extends Base {
|
||||
|
||||
// if the website is ready to accept requests
|
||||
// (protects against users requesting unready resources)
|
||||
private bool $ready;
|
||||
// if the router is currently in an error handler
|
||||
private bool $in_err;
|
||||
// The parsed request. Contains the following fields.
|
||||
//
|
||||
// NAME | TYPE | DEFAULT | DESCRIPTION
|
||||
// ------------------------------------------------------------
|
||||
// app | string | 'index' | The first part of the uri, selects which
|
||||
// | | | controller will be called.
|
||||
// slug | string | 'index' | The second part of the uri, slects
|
||||
// | | | which method will be called.
|
||||
// args | array | [] | Rest of uri parts. Arguments passed to
|
||||
// | | | the method.
|
||||
// uri | array? | | From PHP's parse_url($uri_str).
|
||||
// uri_str | string | | The URI given in the HTML request.
|
||||
// method | string | | The HTTP method.
|
||||
// ip | string | | The requesting IP.
|
||||
public final array $req;
|
||||
|
||||
/**
|
||||
* Creates the crimson router.
|
||||
*
|
||||
* => Checks if crimson is ready to route requests.
|
||||
* => Defines self as ROUTER.
|
||||
*/
|
||||
function __construct()
|
||||
{
|
||||
// are we ready?
|
||||
$this->ready = $this->ready_check();
|
||||
// get request
|
||||
$this->in_err = FALSE;
|
||||
$this->req = $this->get_req();
|
||||
// ROUTER is used by Controller in error($code) function
|
||||
define('ROUTER', $this);
|
||||
// fail if URI is invalid (get_req does not check this)
|
||||
if (!$this->req['uri'])
|
||||
$this->handle_error(400); // does not return!
|
||||
}
|
||||
|
||||
/**
|
||||
* Cheks if crimson is ready to route requests.
|
||||
* => Checks if the database is ready (if enabled).
|
||||
*/
|
||||
private function ready_check(): bool {
|
||||
// assume we are ready unless told otherwise
|
||||
$ready = TRUE;
|
||||
// check db
|
||||
if ($ready && getenv('POSTGRES_ENABLED') === 'true') {
|
||||
$ready = file_exists('/var/run/crimson/db_ready');
|
||||
}
|
||||
// return result
|
||||
return $ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 modified route
|
||||
$routes = CONFIG['routes'];
|
||||
foreach ($routes as $key => $value) {
|
||||
$key = "/^{$key}$/";
|
||||
if (!preg_match($key, $path, $matches))
|
||||
continue;
|
||||
|
||||
$path = $value;
|
||||
|
||||
for ($i = 1; $i < count($matches); $i++) {
|
||||
$path = str_replace(
|
||||
"\\{$i}",
|
||||
$matches[$i],
|
||||
$path);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// get path parts
|
||||
$parts = explode('/', $path);
|
||||
if ($path == '')
|
||||
$parts = [];
|
||||
// get the length
|
||||
$len = count($parts);
|
||||
// get route info
|
||||
$route = array();
|
||||
$route['app'] = $len > 0 ? $parts[0] : 'index';
|
||||
$route['slug'] = $len > 1 ? $parts[1] : 'index';
|
||||
$route['args'] = array_slice($parts, 2);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the users ip
|
||||
*/
|
||||
private function get_ip(): ?string
|
||||
{
|
||||
$headers = array (
|
||||
'HTTP_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'HTTP_X_REAL_IP',
|
||||
'REMOTE_ADDR'
|
||||
);
|
||||
foreach ($headers as $header) {
|
||||
if (isset($_SERVER[$header]))
|
||||
return $_SERVER[$header];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the curret request info.
|
||||
* Does not fail in invalid uri. Must be handeled by
|
||||
* caller function.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function get_req(): array
|
||||
{
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri_str = $_SERVER['REQUEST_URI'];
|
||||
$uri = parse_url($uri_str);
|
||||
|
||||
$path = '';
|
||||
if ($uri && array_key_exists('path', $uri))
|
||||
$path = $uri['path'];
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
'uri' => $uri,
|
||||
'uri_str' => $uri_str,
|
||||
'method' => $method,
|
||||
'ip' => $this->get_ip()
|
||||
),
|
||||
$this->get_req_route($path),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a router error code
|
||||
* @param int $code - the http error code
|
||||
*/
|
||||
public function handle_error(int $code): never
|
||||
{
|
||||
// if in_err is set RIGHT NOW, this means the user specified
|
||||
// error hook errored itself. To prevent error recursion we do
|
||||
// not want to run it again!
|
||||
$force_builtin = $this->in_err;
|
||||
// Sets the in_err catch to true, read comment above to why.
|
||||
$this->in_err = TRUE;
|
||||
CRIMSON_HOOK('error', [$this->req, $code], $force_builtin);
|
||||
// error hook is type never, but CRIMSON_HOOK is type void
|
||||
CRIMSON_DIE();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $req
|
||||
* @param array<int,mixed> $req
|
||||
*/
|
||||
public function handle_req(): never
|
||||
{
|
||||
// block requests if we are not ready
|
||||
if ($this->ready === FALSE)
|
||||
$this->handle_error(503);
|
||||
|
||||
// run pre route hook
|
||||
CRIMSON_HOOK('pre_route', [$this]);
|
||||
|
||||
// load the controller
|
||||
$controller = $this->load_controller($this->req['app']);
|
||||
if (!$controller)
|
||||
$this->handle_error(404);
|
||||
|
||||
// find the function that matches our request
|
||||
// format: /controller/fn_name/fn_arg1/fn_arg2/.../fn_argN
|
||||
$handler = NULL;
|
||||
try {
|
||||
$cls = new ReflectionClass($controller);
|
||||
$mds = $cls->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||
foreach ($mds as $md) {
|
||||
if ($md->name !== $this->req['slug'])
|
||||
continue;
|
||||
if (count($md->getParameters()) !=
|
||||
count($this->req['args']))
|
||||
continue;
|
||||
$handler = $md;
|
||||
break;
|
||||
}
|
||||
} catch (Exception $_e) {}
|
||||
|
||||
// return 404 if no handler found
|
||||
if (!$handler)
|
||||
$this->handle_error(404);
|
||||
|
||||
try {
|
||||
$handler->invokeArgs($controller, $this->req['args']);
|
||||
} catch (Exception $_e) {};
|
||||
|
||||
// sanity check
|
||||
CRIMSON_DIE();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue