Compare commits

...

11 Commits

51 changed files with 1065 additions and 249 deletions

View File

@ -1,4 +1,4 @@
![Novaconium PHP](/_assets/header.svg) ![Novaconium PHP](/_assets/novaconium-logo.png)
# Novaconium PHP: A PHP Framework Built from the Past # Novaconium PHP: A PHP Framework Built from the Past
@ -11,28 +11,25 @@ Master Repo: https://git.4lt.ca/4lt/novaconium
## Getting Started ## Getting Started
### Installation Novaconium is heavly influenced by docker, but you can use composer outside of docker.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Install-Composer-On-Debian.md).
```bash ```bash
mkdir project_name; PROJECTNAME=novaproject;
cd project_name; mkdir -p $PROJECTNAME/novaconium;
# Native cd $PROJECTNAME;
composer require 4lt/novaconium
# Composer
docker run --rm --interactive --tty --volume $PWD:/app composer require 4lt/novaconium
cp -R vendor/4lt/novaconium/defaults/App/ .
cp -R vendor/4lt/novaconium/defaults/public/ .
```
#### Compose install (debian) docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest require 4lt/novaconium;
```bash cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
sudo nala install curl php-cli php-mbstring git unzip
curl -sS https://getcomposer.org/installer -o composer-setup.php # Edit .env
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer # Edit novaconium/App/config.php
rm composer-setup.php
docker compose up -d
``` ```
## Documentation ## Documentation
* [Docker Setup](https://git.4lt.ca/4lt/novaconium) * [Novaconiumm Official Repo](https://git.4lt.ca/4lt/novaconium)
* [CORXN Apache and PHP Container for Novaconium](https://git.4lt.ca/4lt/CORXN)

View File

@ -1,39 +0,0 @@
<svg width="100%" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="black"/>
<defs>
<radialGradient id="star-gradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color: white; stop-opacity: 1" />
<stop offset="100%" style="stop-color: white; stop-opacity: 0" />
</radialGradient>
<g id="star">
<circle cx="0" cy="0" r="2" fill="white" />
</g>
</defs>
<!-- Lots of stars moving outward slower -->
<g>
<use href="#star" x="50%" y="50%" transform="translate(-300,-150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(300,-150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(-300,150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(300,150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(-150,-75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(150,-75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(-150,75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(150,75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(-75,-37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(75,-37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(-75,37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(75,37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(-350,-175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(350,-175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(-350,175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(350,175) scale(0.4)" />
<animateTransform attributeName="transform" type="scale" from="1" to="10" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" from="1" to="0" dur="2s" repeatCount="indefinite" />
</g>
<!-- Centered Title -->
<text x="50%" y="50%" fill="white" font-size="40" text-anchor="middle" font-family="Arial" dy=".3em">
Novaconium PHP
</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

BIN
_assets/novaconium-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,7 +1,6 @@
{ {
"name": "4lt/novaconium", "name": "4lt/novaconium",
"description": "A high-performance PHP framework built from the past.", "description": "A high-performance PHP framework built from the past.",
"version": "1.0.3",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {

20
config/routes.php Normal file
View File

@ -0,0 +1,20 @@
<?php
$framework_routes = [
'/novaconium' => [
'get' => 'NOVACONIUM/init'
],
'/novaconium/create_admin' => [
'post' => 'NOVACONIUM/create_admin'
],
'/novaconium/login' => [
'post' => 'NOVACONIUM/authenticate',
'get' => 'NOVACONIUM/login'
],
'/novaconium/dashboard' => [
'get' => 'NOVACONIUM/dashboard'
],
'/novaconium/logout' => [
'post' => 'NOVACONIUM/logout',
'get' => 'NOVACONIUM/logout'
]
];

View File

@ -0,0 +1,70 @@
<?php
use Nickyeoman\Validation;
$v = new Nickyeoman\Validation\Validate();
$url_success = '/novaconium/dashboard';
$url_fail = '/novaconium/login';
// Don't go further if already logged in
if ( !empty($session->get('username')) ) {
$redirect->url($url_success);
makeitso();
}
// Make sure Session Token is correct
if ($session->get('token') != $post->get('token')) {
$messages->addMessage('error', "Invalid Session.");
$log->error("Login Authentication - Invalid Session Token");
}
// Handle Username
$rawUsername = $post->get('username', null);
$cleanUsername = $v->clean($rawUsername); // Clean the input
$username = strtolower($cleanUsername); // Convert to lowercase
if (!$username) {
$messages->addMessage('error', "No Username given.");
}
// Handle Password
$password = $v->clean($post->get('password', null));
if ( empty($password) ) {
$messages->addMessage('error', "Password Empty.");
}
/*************************************************************************************************************
* Query Database
************************************************************************************************************/
if ($messages->count('error') === 0) {
$query = "SELECT id, username, email, password, blocked FROM users WHERE username = ? OR email = ?";
$matched = $db->getRow($query, [$username, $username]);
if (empty($matched)) {
$messages->addMessage('error', "User or Password incorrect.");
$log->warning("Login Authentication - Login Error, user doesn't exist");
}
}
if ($messages->count('error') === 0) {
// Re-apply pepper
$peppered = hash_hmac('sha3-512', $password, $config['secure_key']);
// Verify hashed password
if (!password_verify($peppered, $matched['password'])) {
$messages->addMessage('error', "User or Password incorrect.");
$log->warning("Login Authentication - Login Error, password wrong");
}
}
// Process Login or Redirect
if ($messages->count('error') === 0) {
$query = "SELECT groupName FROM user_groups WHERE user_id = ?";
$groups = $db->getRow($query, [$matched['id']]);
$session->set('username', $cleanUsername);
$session->set('group', $groups['groupName']);
$redirect->url($url_success);
$log->info("Login Authentication - Login Success);
} else {
$redirect->url($url_fail);
}

View File

@ -0,0 +1,58 @@
<?php
use Nickyeoman\Validation;
$validate = new Validation\Validate();
$valid = true;
$p = $post->all();
// Check secure key
if (empty($p['secure_key']) || $p['secure_key'] !== $config['secure_key']) {
$valid = false;
}
// Username
$name = $validate->clean($p['username']);
if (!$validate->minLength($name, 1)) {
$valid = false;
}
// Email
if (empty($p['email'])) {
$valid = false;
} elseif (!$validate->isEmail($p['email'])) {
$valid = false;
}
// Password
if (empty($p['password'])) {
$valid = false;
} else {
// Use pepper + Argon2id
$peppered = hash_hmac('sha3-512', $p['password'], $config['secure_key']);
$hashed_password = password_hash($peppered, PASSWORD_ARGON2ID);
}
if ($valid) {
// Insert user
$query = <<<EOSQL
INSERT INTO `users`
(`username`, `password`, `email`, `validate`, `confirmationToken`, `reset`, `created`, `updated`, `confirmed`, `blocked`)
VALUES
(?, ?, ?, NULL, NULL, NULL, NOW(), NOW(), 1, 0);
EOSQL;
$params = [$name, $hashed_password, $p['email']];
$db->query($query, $params);
$userid = $db->lastid();
// Assign admin group
$groupInsertQuery = <<<EOSQL
INSERT INTO `user_groups` (`user_id`, `groupName`) VALUES (?, ?);
EOSQL;
$db->query($groupInsertQuery, [$userid, 'admin']);
}
// Always redirect at end
$redirect->url('/novaconium');

10
controllers/dashboard.php Normal file
View File

@ -0,0 +1,10 @@
<?php
$data['title'] = 'Novaconium Dashboard Page';
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
view('@novacore/dashboard', $data);

128
controllers/init.php Normal file
View File

@ -0,0 +1,128 @@
<?php
$data = [
'secure_key' => false,
'gen_key' => NULL,
'users_created' => false,
'empty_users' => false,
'show_login' => false,
'token' => $session->get('token'),
'title' => 'Novaconium Admin'
];
// Check if SECURE KEY is Set in
if ($config['secure_key'] !== null && strlen($config['secure_key']) === 64) {
$data['secure_key'] = true;
} else {
$data['gen_key'] = substr(bin2hex(random_bytes(32)), 0, 64);
$log->warn('secure_key not detected');
}
// Check if user table exists
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'users';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(30) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`validate` varchar(32) DEFAULT NULL,
`confirmationToken` varchar(255) DEFAULT NULL,
`reset` varchar(32) DEFAULT NULL,
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`confirmed` tinyint(1) NOT NULL DEFAULT 0,
`blocked` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$data['users_created'] = true;
$log->info('Users Table Created');
}
// Check Usergroup
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'user_groups';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `user_groups` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) UNSIGNED NOT NULL,
`groupName` VARCHAR(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
EOSQL;
$db->query($query);
$log->info('User_groups Table Created');
}
// Check Pages Table
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'pages';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `pages` (
`id` int(11) NOT NULL,
`title` varchar(255) NOT NULL,
`heading` varchar(255) NOT NULL,
`description` varchar(255) NOT NULL,
`keywords` varchar(255) NOT NULL,
`author` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`path` varchar(255) DEFAULT NULL,
`intro` text DEFAULT NULL,
`body` text DEFAULT NULL,
`notes` text DEFAULT NULL,
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`draft` tinyint(1) NOT NULL DEFAULT 1,
`changefreq` varchar(7) NOT NULL DEFAULT 'monthly',
`priority` float(4,1) NOT NULL DEFAULT 0.0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$log->info('Pages Table Created');
}
// Check if a user exists
$result = $db->query("SELECT COUNT(*) as total FROM users");
$row = $result->fetch_assoc();
if ($row['total'] < 1) {
$data['empty_users'] = true;
} else {
$log->info('Init Run complete, all sql tables exist with a user.');
// Everything is working, send them to login page
$redirect->url('/novaconium/login');
makeitso();
}
view('@novacore/init', $data);

9
controllers/login.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$data['title'] = 'Novaconium Login Page';
// Don't come here if logged in
if ($session->get('username')) {
$redirect->url('/novaconium/dashboard');
makeitso();
}
view('@novacore/login');

5
controllers/logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
$session->kill();
$log->info("Logout - Logout Success - " . $_SERVER['REMOTE_ADDR']);
$redirect->url('/');
makeitso();

View File

@ -1,11 +0,0 @@
<?php
$config = [
'database' => [
'host' => '',
'name' => '',
'user' => '',
'pass' => '',
'port' => 3306
],
'base_url' => 'http://localhost:8000'
];

View File

@ -1,7 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>This is twig</h1>
<p>Content Here</p>
{% endblock %}

18
docs/Composer.md Normal file
View File

@ -0,0 +1,18 @@
# PHP Composer Cheatsheet
Install novaconium with composer: ```composer require 4lt/novaconium```
Install novaconium with composer in docker: ```docker run --rm --interactive --tty --volume $PWD:/app composer:latest require 4lt/novaconium```
Update novaconium with composer in docker: ```docker run --rm --interactive --tty --volume $PWD:/app composer:latest update```
## Install Composer natively on Debian
Assuming you have nala installed:
```bash
sudo nala install curl php-cli php-mbstring git unzip
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
```

19
docs/Logs.md Normal file
View File

@ -0,0 +1,19 @@
# Logging
You can use the logging class to output to a file.
use ```$log->info(The Message');```1
Logging levels are:
```
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
];
```
It's recommended that production is set to ERROR.
You set the log level in /App/config.php under 'loglevel' => 'ERROR'
If you are using CORXN a health check is run every 30 seconds which would fill the log file with info.

3
docs/Messages.md Normal file
View File

@ -0,0 +1,3 @@
# Messages
Messages is $messages.

5
docs/Post.md Normal file
View File

@ -0,0 +1,5 @@
# Post
There is a post class.
It cleans the post.
You can access it with $post.

6
docs/Redirect.md Normal file
View File

@ -0,0 +1,6 @@
# Redirect
How to use redirect class.
$redirect->url;
it's called on every page, if you set it more than once the last one is used.

5
docs/Session.md Normal file
View File

@ -0,0 +1,5 @@
# Sessions
There is a sessions handler built into Novaconium.
$session

21
docs/Twig-Views.md Normal file
View File

@ -0,0 +1,21 @@
# Twig
## Overrides
You can override twig templates by creating the same file in the templates directory.
## Calling View
There is a $data that the system uses to store arrays for twig you can save to this array:
```
$data['newinfo'] = 'stuff';
view('templatename');
```
and that will automotically go to twig.
or you can create a new array and pass it in:
```
$anotherArr['newinfo'] = 'stuff';
view('templatename',$anotherArr);
```

View File

@ -1,71 +1,8 @@
# Getting Started With Docker # Docker Cheatsheet (for Novaconium)
## Clone Docker Cookbooks ## Sample Docker Compose File
[Github Docker Compose Cookbooks](https://github.com/nickyeoman/docker-compose-cookbooks)
```bash
git clone git@github.com:nickyeoman/docker-compose-cookbooks.git /docker-compose-cookbooks
```
## Setup Docker Compose File
Read the sample extends file of /docker-compose-cookbooks/phpcontainer/sample-extends.yml
```bash
# ensure docker-compose exists
[[ -f docker-compose.yml ]] || echo "services:" > docker-compose.yml
# PHP container
tail -n+2 /docker-compose-cookbooks/phpcontainer/sample-extends.yml >> docker-compose.yml
# PHP settings
cp -r /docker-compose-cookbooks/phpcontainer/config .
# Set project directory
sed -i 's|- "./project:/data"|- "./:/data"|' docker-compose.yml
# Mariadb container
tail -n +2 /docker-compose-cookbooks/mariadb/sample-extends.yml >> docker-compose.yml
```
## ENV File
Then setup the .env file, which should look something like this:
```
COOKBOOK=/docker-compose-cookbooks
COMPOSE_PROJECT_NAME=myProject
TZ=America/Vancouver
VOL_CONFIG_PATH=/data/myProject/config
VOL_PATH=/data/myProject/data
# PHP Container
PHPCONTAINER_IMAGE=4lights/phpcontainer:latest
# MariaDB
MARIADB_IMAGE=mariadb:latest
MARIADB_MARIADB_DATABASE=mydb
MARIADB_MARIADB_ROOT_PASSWORD=ChangeThisPassword0123456789ABCD
MARIADB_MARIADB_PASSWORD=AlsoChangeThisPassword0123456789
MARIADB_MARIADB_USER=dbuser
```
## APP Database config
Open the /App/config.php file and change the database section to match the above:
```php
'database' => [
'host' => 'mariadb',
'name' => 'mydb',
'user' => 'dbuser',
'pass' => 'AlsoChangeThisPassword0123456789',
'port' => 3306
],
```
See the skeleton directory for an example docker setup.
## Start Docker ## Start Docker

View File

@ -1,3 +0,0 @@
# Twig Overrides
You can override twig templates by creating the same file in the templates directory.

9
services/Auth.php Normal file
View File

@ -0,0 +1,9 @@
<?php
class Auth
{
public function __construct()
{
}
}

2
skeleton/.env Normal file
View File

@ -0,0 +1,2 @@
MYSQL_ROOT_PASSWORD=random
MYSQL_PASSWORD=random

3
skeleton/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
data/
novaconium/vendor/
novaconium/logs/

View File

@ -0,0 +1,56 @@
# Sample Docker Compose
services:
corxn:
image: 4lights/corxn:6.0.0
ports:
- "8000:80"
volumes:
- ./novaconium:/data
- ./data/logs:/var/log/apache2 # Optional Logs
restart: unless-stopped
networks:
- internal
- proxy
redis:
image: redis:latest
networks:
- internal
restart: unless-stopped
mariadb:
image: mariadb:latest
container_name: mariadb
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: novadb
MYSQL_USER: novaconium
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- ./data/db:/var/lib/mysql
networks:
- internal
restart: unless-stopped
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
restart: unless-stopped
ports:
- "8001:80"
networks:
- internal
environment:
- PMA_ARBITRARY=-1
- PMA_HOST=mariadb
- PMA_USER=root
- PMA_PASSWORD=${MYSQL_ROOT_PASSWORD}
- UPLOAD_LIMIT=200M
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
proxy:
external: true
internal:
driver: bridge

View File

@ -0,0 +1,14 @@
<?php
$config = [
'database' => [
'host' => 'mariadb',
'name' => 'novadb',
'user' => 'novaconium',
'pass' => '',
'port' => 3306
],
'base_url' => 'http://localhost:8000',
'secure_key' => '', //64 alphanumeric characters
'logfile' => '/logs/novaconium.log',
'loglevel' => 'ERROR' // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE'
];

View File

@ -0,0 +1,21 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<img src="https://git.4lt.ca/4lt/novaconium/media/branch/master/_assets/novaconium-logo.png" aalt="Novaconium framework logo" />
<h2>Minimalist PHP framework</h2>
<p>
Edit <code>App/routes.php</code> and <code>App/controllers/index.php</code><br>
to customize this page.
</p>
<p>Sign in to the <a href="/novaconium">Administration</a></p>
<h2>Documentation</h2>
<ul>
<li>Style Sheets</li>
<li><a href="https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/twig-overrides.md">Twig overrides</a></li>
</ul>
<h2>Repository</h2>
<p class="small">Visit Source Control Repository for <a href="https://git.4lt.ca/4lt/novaconium">Novaconium</a></p>
{% endblock %}

View File

@ -0,0 +1,47 @@
body {
background-color: #1b1f23;
width: 100%;
font-family: 'Fira Code', 'Source Code Pro', monospace;
}
article {
max-width: 900px;
border: 1px solid #3b444c;
background-color: #14171a;
color: #fff;
margin: 0 auto;
padding: 20px;
margin-top: 50px;
}
code {
font-family: 'Fira Code', monospace;
font-size: 13px;
background-color: #0d1117;
color: #c9d1d9;
padding: 0.2em 0.4em;
border-radius: 6px;
border: 1px solid #30363d;
}
.small {
font-size: 10px;
}
h2 {
margin-top: 40px;
}
div.error, div#debug {
border: 1px solid red;
padding: 30px;
background-color: pink;
color: darkred;
margin: 0 auto;
width: 900px;
}
div#debug {
margin-top: 100px;
margin-bottom: 100px;
}

View File

@ -2,71 +2,122 @@
class Database { class Database {
private $host;
private $user;
private $pass;
private $dbname;
private $conn; private $conn;
public $lastid;
public function __construct($dbinfo) { public function __construct($dbinfo) {
$this->host = $dbinfo['host']; $this->conn = new mysqli($dbinfo['host'], $dbinfo['user'], $dbinfo['pass'], $dbinfo['name']);
$this->user = $dbinfo['user'];
$this->pass = $dbinfo['pass'];
$this->dbname = $dbinfo['name'];
$this->connect();
}
private function connect() {
$this->conn = new mysqli($this->host, $this->user, $this->pass, $this->dbname);
if ($this->conn->connect_error) { if ($this->conn->connect_error) {
die("Connection failed: " . $this->conn->connect_error); die("Connection failed: " . $this->conn->connect_error);
} }
} }
public function query($query) { public function query($query, $params = []) {
// Clean up pending results to avoid "commands out of sync"
while ($this->conn->more_results() && $this->conn->next_result()) {
if ($res = $this->conn->use_result()) {
$res->free();
}
}
// Prepare the SQL statement
$stmt = $this->conn->prepare($query); $stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters if needed
if (!empty($params)) {
$types = str_repeat('s', count($params)); // Use 's' for all types, or detect types dynamically
$stmt->bind_param($types, ...$params);
}
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
}
// Save last insert id if it's an INSERT query
if (preg_match('/^\s*INSERT/i', $query)) {
$this->lastid = $this->conn->insert_id;
} else {
$this->lastid = 0;
}
// Decide what to return
// For SELECT/SHOW etc., return result set
if (preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)/i', $query)) {
$result = $stmt->get_result();
$stmt->close();
return $result;
}
// For INSERT/UPDATE/DELETE, return success status
$success = $stmt->affected_rows;
$stmt->close();
return $success;
}
public function lastid() {
return $this->lastid;
}
public function getRow($query, $params = []) {
try {
// Prepare the SQL statement
$stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters
if (!empty($params)) {
$types = str_repeat('s', count($params)); // You may improve this with actual type detection
$stmt->bind_param($types, ...$params);
}
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
}
// Get result
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$stmt->close();
return $row;
} catch (Exception $e) {
echo "An error occurred: " . $e->getMessage();
return null;
}
}
public function getRows($query, $params = []) {
$stmt = $this->conn->prepare($query);
if (!$stmt) {
die("Query preparation failed: " . $this->conn->error);
}
// Bind parameters if provided
if (!empty($params)) {
$types = str_repeat('s', count($params)); // Assuming all are strings, adjust as needed
$stmt->bind_param($types, ...$params);
}
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); // Requires MySQL Native Driver (mysqlnd)
return $stmt->get_result();
}
public function getRow($query) {
$result = $this->query($query);
return $result->fetch_assoc();
}
public function debugGetRow($query) {
echo "<h1>Debug GetRow Query</h1>";
echo "<div class='debug-query'>Query: $query</div>";
$result = $this->query($query);
$row = $result->fetch_assoc();
echo "<pre>"; if ($result) {
print_r($row); return $result->fetch_all(MYSQLI_ASSOC);
echo "</pre>"; } else {
return [];
die(); }
}
public function getRows($query) {
$result = $this->query($query);
return $result->fetch_all(MYSQLI_ASSOC);
}
public function debugGetRows($query) {
echo "<h1>Debug GetRows Query</h1>";
echo "<div class='debug-query'>Query: $query</div>";
$result = $this->query($query);
$rows = $result->fetch_all(MYSQLI_ASSOC);
echo "<pre>";
print_r($rows);
echo "</pre>";
die();
} }
public function close() { public function close() {

48
src/Logger.php Normal file
View File

@ -0,0 +1,48 @@
<?php
class Logger {
protected string $logFile;
protected int $logLevelThreshold;
const LEVELS = [
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
'NONE' => 999
];
public function __construct(string $logFile, string $minLevel = 'DEBUG') {
$this->logFile = $logFile;
$minLevel = strtoupper($minLevel);
$this->logLevelThreshold = self::LEVELS[$minLevel] ?? 0;
}
protected function log(string $level, string $message): void {
$level = strtoupper($level);
if (!isset(self::LEVELS[$level]) || self::LEVELS[$level] < $this->logLevelThreshold) {
return;
}
$time = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'];
$logEntry = "[$time] [$ip] [$level] $message" . PHP_EOL;
file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
public function debug(string $msg): void {
$this->log('DEBUG', $msg);
}
public function info(string $msg): void {
$this->log('INFO', $msg);
}
public function warning(string $msg): void {
$this->log('WARNING', $msg);
}
public function error(string $msg): void {
$this->log('ERROR', $msg);
}
}

83
src/MessageHandler.php Normal file
View File

@ -0,0 +1,83 @@
<?php
class MessageHandler {
private $messages = [
'error' => [],
'warning' => [],
'notice' => [],
'success' => []
];
public function __construct(array $sessionMessages = [])
{
// Merge existing session messages into the default structure
foreach ($this->messages as $type => $_) {
if (isset($sessionMessages[$type]) && is_array($sessionMessages[$type])) {
$this->messages[$type] = $sessionMessages[$type];
}
}
}
// Add a message of a specific type
public function addMessage($type, $message) {
if (!isset($this->messages[$type])) {
throw new Exception("Invalid message type: $type");
}
$this->messages[$type][] = $message;
}
public function error($message){
$this->addMessage('error', $message);
}
// Get all messages of a specific type
public function getMessages($type) {
return $this->messages[$type] ?? [];
}
// Get all messages of a specific type
public function showMessages($type) {
$result = $this->messages[$type] ?? [];
$this->messages[$type] = []; // Clear messages after showing
return $result;
}
// Get all messages of all types
public function getAllMessages() {
return $this->messages;
}
// Get the count of messages for a specific type
public function count($type) {
return isset($this->messages[$type]) ? count($this->messages[$type]) : 0;
}
// Get the total count of all messages
public function totalCount() {
return array_sum(array_map('count', $this->messages));
}
// Check if there are any messages of a specific type
public function hasMessages($type) {
return !empty($this->messages[$type]);
}
// Check if there are any messages at all
public function hasAnyMessages() {
return $this->totalCount() > 0;
}
// Clear messages of a specific type
public function clear($type) {
if (isset($this->messages[$type])) {
$this->messages[$type] = [];
}
}
// Clear all messages
public function clearAll() {
foreach ($this->messages as $type => $list) {
$this->messages[$type] = [];
}
}
}

25
src/Post.php Normal file
View File

@ -0,0 +1,25 @@
<?php
class Post {
private $data = [];
public function __construct($post) {
$this->sanitize($post);
}
private function sanitize($post) {
foreach ($post as $key => $value) {
$this->data[$key] = is_array($value)
? filter_var_array($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS)
: filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
}
}
public function get($key, $default = null) {
return $this->data[$key] ?? $default;
}
public function all() {
return $this->data;
}
}

40
src/Redirect.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/**
* Use
* $redirect->url('/login');
* to trigger a redirect
*/
class Redirect {
private ?string $url = null;
private int $statusCode = 303;
public function url(string $relativeUrl, int $statusCode = 303): void {
$this->statusCode = $statusCode;
// Detect HTTPS
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
// Get Hostname
$host = $_SERVER['HTTP_HOST'];
// Get Base Directory
$basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
// Construct Absolute URL
$this->url = "$protocol://$host$basePath/" . ltrim($relativeUrl, '/');
}
public function isset(): bool {
return !is_null($this->url);
}
public function execute(): void {
if ($this->url) {
header("Location: " . $this->url, true, $this->statusCode);
exit();
}
}
}

View File

@ -19,12 +19,13 @@ class Router {
} }
private function loadRoutes() { private function loadRoutes() {
require_once(FRAMEWORKPATH . '/config/routes.php');
// Check if Path exists // Check if Path exists
if (file_exists(BASEPATH . '/App/routes.php')) { if (file_exists(BASEPATH . '/App/routes.php')) {
require_once( BASEPATH . '/App/routes.php'); require_once( BASEPATH . '/App/routes.php');
} else {
require_once(FRAMEWORKPATH . '/defaults/App/routes.php');
} }
$routes = array_merge((array)$routes, (array)$framework_routes);
return $routes; return $routes;
} }
@ -67,7 +68,9 @@ class Router {
// one to one match // one to one match
if (array_key_exists($this->path, $this->routes)) { if (array_key_exists($this->path, $this->routes)) {
return $this->routes[$this->path][$this->requestType]; if (!empty($this->routes[$this->path][$this->requestType])) {
return $this->routes[$this->path][$this->requestType];
}
} }
foreach ($this->routes as $key => $value) { foreach ($this->routes as $key => $value) {
@ -103,8 +106,13 @@ class Router {
// checks if the file exists, sets file path // checks if the file exists, sets file path
private function setRouteFile() { private function setRouteFile() {
$cp = BASEPATH . '/App/controllers/' . $this->controller . '.php'; if (str_starts_with($this->controller, 'NOVACONIUM')) {
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
$cp = FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
} else {
$cp = BASEPATH . '/App/controllers/' . $this->controller . '.php';
}
if (file_exists($cp)) { if (file_exists($cp)) {
return $cp; return $cp;
@ -119,19 +127,16 @@ class Router {
} }
public function debug() { public function debug() {
echo '<h1>Debugging Router</h1>'; echo '<div id="router-debug-container" class="debug">';
echo '<h2>Url Path</h2>'; echo '<table border="1" cellpadding="10" cellspacing="0">';
echo $this->path . '<br>'; echo '<tr><th>Url Path</th><td>' . htmlspecialchars($this->path) . '</td></tr>';
echo '<h2>ControllerPath</h2>'; echo '<tr><th>Controller Path</th><td>' . htmlspecialchars($this->controllerPath) . '</td></tr>';
echo $this->controllerPath; echo '<tr><th>Parameters</th><td><pre>' . print_r($this->parameters, true) . '</pre></td></tr>';
echo '<h2>Parameters</h2>'; echo '<tr><th>Routes</th><td><pre>' . print_r($this->routes, true) . '</pre></td></tr>';
echo '<pre>'; echo '</table></div>';
print_r($this->parameters);
echo '</pre>';
echo '<h2>Routes Variable</h2><pre>';
print_r($this->routes);
echo '</pre>';
die(); die();
} }
} }

View File

@ -4,13 +4,14 @@ class Session {
private $session; private $session;
public function __construct() { public function __construct() {
if (!isset($_SESSION)) { session_start();
session_start(); if (!isset($_SESSION['token'])) {
$this->session = $_SESSION; $this->setToken();
$this->session['messages'] = [];
} else { } else {
$this->session = $_SESSION; $this->session = $_SESSION;
} }
$this->setToken();
} }
public function setToken() { public function setToken() {
@ -34,7 +35,7 @@ class Session {
} }
public function debug() { public function debug() {
print_r($this->session); return $this->session;
} }
public function delete($key) { public function delete($key) {
@ -48,4 +49,9 @@ class Session {
session_write_close(); session_write_close();
} }
public function kill() {
$_SESSION = [];
session_destroy();
}
} }

29
src/functions.php Normal file
View File

@ -0,0 +1,29 @@
<?php
/**
* Dump and Die
*/
function dd(...$vars) {
echo "<pre style='background:#222;color:#0f0;padding:10px;border-radius:5px;'>";
foreach ($vars as $var) {
var_dump($var);
echo "\n";
}
echo "</pre>";
die();
}
function makeitso() {
global $session, $db, $redirect, $config, $messages, $log;
if (!empty($config['database']['host'])) {
$db->close();
}
$session->set('messages', $messages->getAllMessages());
$session->write();
$redirect->execute();
exit();
}

View File

@ -10,12 +10,32 @@ if (file_exists(BASEPATH . '/App/config.php')) {
require_once(FRAMEWORKPATH . '/defaults/App/config.php'); require_once(FRAMEWORKPATH . '/defaults/App/config.php');
} }
// Creates twig and the view() function // Logging
require_once(FRAMEWORKPATH . '/src/Logger.php');
$log = new Logger(BASEPATH . $config['logfile'], $config['loglevel']);
// Global Functions
require_once(FRAMEWORKPATH . '/src/functions.php');
// Creates the view() function using twig
$data = array();
require_once(FRAMEWORKPATH . '/src/twig.php'); require_once(FRAMEWORKPATH . '/src/twig.php');
// Start a Session // Start a Session
require_once(FRAMEWORKPATH . '/src/Session.php'); require_once(FRAMEWORKPATH . '/src/Session.php');
$session = new Session(); $session = new Session();
$data['token'] = $session->get('token');
if ($config['loglevel'] == 'DEBUG') {
$data['debug'] = nl2br(print_r($session->debug(), true));
}
// Messages
require_once(FRAMEWORKPATH . '/src/MessageHandler.php');
$messages = new MessageHandler($session->flash('messages'));
foreach (['error','notice'] as $key){
$data[$key] = $messages->showMessages($key);
}
// Load Database Class // Load Database Class
if (!empty($config['database']['host'])) { if (!empty($config['database']['host'])) {
@ -23,11 +43,20 @@ if (!empty($config['database']['host'])) {
$db = new Database($config['database']); $db = new Database($config['database']);
} }
// Sanatize POST Data
if (!empty($_POST)) {
require_once(FRAMEWORKPATH . '/src/Post.php');
$post = new POST($_POST);
}
// Start a Redirect
require_once(FRAMEWORKPATH . '/src/Redirect.php');
$redirect = new Redirect();
// Load a controller // Load a controller
require_once(FRAMEWORKPATH . '/src/Router.php'); require_once(FRAMEWORKPATH . '/src/Router.php');
$router = new Router(); $router = new Router();
//$router->debug(); //$router->debug();
require_once($router->controllerPath); require_once($router->controllerPath);
//write the session makeitso();
$session->write();

View File

@ -1,11 +1,16 @@
<?php <?php
//Twig //Twig
function view($name = '', $data = []) { function view($name = '', $moreData = []) {
global $config; // Use the globally included $config global $config, $data; // Use the globally included $config
if (!empty($moreData)){
$data = array_merge($data, $moreData);
}
$loader = new Twig\Loader\FilesystemLoader(BASEPATH . '/App/views/'); $loader = new Twig\Loader\FilesystemLoader(BASEPATH . '/App/views/');
$loader->addPath(BASEPATH . '/vendor/4lt/novaconium/twig', 'novaconium'); $loader->addPath(FRAMEWORKPATH . '/twig', 'novaconium');
$loader->addPath(FRAMEWORKPATH . '/views', 'novacore');
$loader->addPath(BASEPATH . '/App/templates', 'override'); $loader->addPath(BASEPATH . '/App/templates', 'override');
$twig = new Twig\Environment($loader); $twig = new Twig\Environment($loader);
@ -15,7 +20,10 @@ function view($name = '', $data = []) {
// Check if the template exists // Check if the template exists
if (file_exists(BASEPATH . '/App/views/' . $name . '.html.twig')) { if (file_exists(BASEPATH . '/App/views/' . $name . '.html.twig')) {
echo $twig->render("$name.html.twig", $data); echo $twig->render($name . '.html.twig', $data);
return true;
} elseif (str_starts_with($name, '@')) { // Check if using framework
echo $twig->render($name . '.html.twig', $data);
return true; return true;
} else { } else {
echo "Error: Twig Template ($name) Not Found."; echo "Error: Twig Template ($name) Not Found.";

View File

@ -1,5 +1,5 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ title | default('Welcome') }}</title> <title>{{ title | default('Welcome To Novaconium') }}</title>
<meta name="generator" content="Novaconium" /> <meta name="generator" content="Novaconium" />
<meta name="description" content="{{ description | default('No description given') }}"> <meta name="description" content="{{ description | default('No description given') }}">
@ -19,8 +19,8 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
{# https://developers.google.com/fonts/docs/getting_started #} {# https://developers.google.com/fonts/docs/getting_started #}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Material+Icons|Material+Icons+Outlined"> <link href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Source+Code+Pro&display=swap&family=Material+Icons&family=Material+Icons+Outlined" rel="stylesheet">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/novaconium.css">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">

View File

@ -47,6 +47,13 @@
{% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %} {% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %}
{% block footerafter %}{% endblock %} {% block footerafter %}{% endblock %}
</footer> </footer>
{% if debug is not empty %}
<div id="debug">
<h2>Debugging Information</h2>
{{ debug|raw }}
</div>
{% endif %}
{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %} {% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %}
</body></html> </body></html>

View File

@ -1,21 +1,5 @@
<div class="container"> <div id="topnav">
<div id="logo"><a href="/">Logo Goes Here</a></div>
<nav> <nav>
<! -- Navigation Goes Here -->
<ul>
<li><a href="/">Home</a></li>
<li><a href="/contact">Contact Us</a></li>
{% if loggedin|default(false) %}
<li><a href="/logout/">Logout</a></li>
{% else %}
<li><a href="/login/">Login</a></li>
{% endif %}
{% if admin|default(false) == 'admin' %}
<li><a href="/admin">Admin</a></li>
{% endif %}
</ul>
</nav> </nav>
</div> </div>

View File

@ -0,0 +1,9 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
<p>Dashboard page</p>
<p><a href="/">Homepage</a></p>
<p><a href="/novaconium/logout">logout</p>
{% endblock %}

67
views/init.html.twig Normal file
View File

@ -0,0 +1,67 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
{% if not secure_key %}
<div id="secure_key">
<h2>Secure Key</h2>
<p>Please set the <code>secure_key</code> in <code>App/config.php</code> to an alphanumeric code that is 64 characters in length.</p>
<p>You can generate a secure key like this:</p>
<pre><code>pwgen -sB 64 1</code></pre>
<p>Or use this one:</p>
<pre><code>{{gen_key}}</code></pre>
</div>
{% endif %}
{% if users_created %}
<div id="users_created">
<h2>Users Table Created</h2>
<p>There was no users table in the database. One was created.</p>
</div>
{% endif %}
{% if empty_users and secure_key %}
<div id="users_created">
<h2>Create Admin</h2>
<p>No admin users exist, make an admin user now.</p>
<form method="post" action="/novaconium/create_admin">
<input type="hidden" name="token" value="{{ token }}" />
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="email">Email:</label><br>
<input type="text" id="email" name="email" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<label for="secure_key">Secure Key <i>The <code>secure_key</code> from your config.php file</i>:</label><br>
<input type="text" id="secure_key" name="secure_key" required><br><br>
<button type="submit">Create User</button>
</form>
</div>
{% endif %}
{% if show_login %}
<div id="users_created">
<h2>Administrator Login</h2>
<form method="post" action="/novaconium/login">
<input type="hidden" name="token" value="{{ token }}" />
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<button type="submit">Login</button>
</form>
</div>
{% endif %}
{% endblock %}

23
views/login.html.twig Normal file
View File

@ -0,0 +1,23 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
<div id="login_form">
<form method="post" action="/novaconium/login">
<input type="hidden" name="token" value="{{ token }}" />
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<button type="submit">Login</button>
</form>
</div>
{% endblock %}