trying to port web app starter from PHP

This commit is contained in:
udo
2026-04-19 09:38:23 +00:00
parent 46d98a092f
commit be514d63d6
546 changed files with 76910 additions and 2807 deletions
+199
View File
@@ -0,0 +1,199 @@
<?php
// Example component definition file used by the components system:
/*<?php return [
'begin' => function($prop, &$context) {
$context['current_component_id'] = $prop['id'];
return('<div class="my-component">');
},
'end' => function($prop) {
?><script>
console.log('Component <?= safe($prop['id']) ?> finalized');
</script><?php
return('</div>');
},
'render' => function($prop, &$context) {
return($context['begin']($prop, $context) . $context['end']($prop));
},
'about' => 'This is an example component that wraps content in a div and logs when it is finalized.',
]; */
$GLOBALS['render_funcs'] = array();
$GLOBALS['id_counter'] = $GLOBALS['id_counter'] ?? 1;
function component_error_banner($s)
{
return '<div class="banner">'.safe($s).'</div>';
}
function component_caller_dir()
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
return isset($trace[1]['file']) ? dirname($trace[1]['file']) : getcwd();
}
function component_normalize_name($file_name)
{
return preg_replace('/\.php$/', '', trim((string)$file_name));
}
function component_resolve_file($file_name, $search_path = false)
{
$component_name = component_normalize_name($file_name);
$candidates = array($component_name.'.php');
if($search_path && !str_starts_with($component_name, 'components/'))
$candidates[] = rtrim($search_path, '/').'/'.$component_name.'.php';
if(!str_starts_with($component_name, 'components/'))
$candidates[] = 'components/'.$component_name.'.php';
foreach(array_unique($candidates) as $candidate)
{
if(file_exists($candidate))
return $candidate;
}
return false;
}
/**
* Loads a component from file system and registers it in the global registry
*
* Component files are searched in this order:
* 1. exact file path passed to the loader
* 2. caller-relative path (for shorthand component names)
* 3. components/{component_name}.php
*
* Component files should return an array with render functions like:
* return ['render' => function($prop) { ... }];
*/
function component_get_func($file_name, $return_false_if_not_found = false, $search_path = false)
{
$component_name = component_normalize_name($file_name);
$result = [];
// Check if component is already loaded in global registry
if(!isset($GLOBALS['render_funcs'][$component_name]))
{
$component_file = component_resolve_file($component_name, $search_path);
if($component_file)
{
$result['component_file'] = $component_file;
}
else
{
// Component file not found - create error component
if($return_false_if_not_found) return(false);
$result['component_file'] = false;
$result['error'] = 'Component not found: '.$component_name;
$result['render'] = function() use($component_name) {
return component_error_banner('component not found: '.$component_name);
};
}
if($result['component_file'])
foreach(require($result['component_file']) as $k => $v)
$result[$k] = $v;
// Register component in global registry for future use
$GLOBALS['render_funcs'][$component_name] = $result;
}
return($GLOBALS['render_funcs'][$component_name]);
}
/**
* Checks if a component exists without loading it
* Returns true if component file can be found, false otherwise
*/
function component_exists($file_name)
{
$caller_dir = component_caller_dir();
return(component_get_func($file_name, true, $caller_dir) !== false);
}
/**
* Loads a component and returns its definition
* Same as component_get_func but with caller directory detection
*/
function component_load($file_name)
{
$caller_dir = component_caller_dir();
return(component_get_func($file_name, true, $caller_dir));
}
function component_call($file_name, $render_call, $prop = array())
{
$prop['render_call'] = $render_call;
return component($file_name, $prop);
}
/**
* Declares an inline component directly in code (not from file)
*
* @param string $file_name - Component name for registry
* @param array $render_prop - Component definition with render functions
*
* Example:
* component_declare('my-button', [
* 'render' => function($prop) { return "<button>{$prop['text']}</button>"; }
* ]);
*/
function component_declare($file_name, $render_prop)
{
// Register the inline component directly in global registry
$GLOBALS['render_funcs'][$file_name] = $render_prop;
}
/**
* Main component rendering function - the heart of the component system
*
* @param string $file_name - Component name or path
* @param array $prop - Properties/data to pass to component
* @return mixed - Component output from the selected render method
*
* Component calling examples:
* component('components/example/theme-switcher')
* component_call('components/workspace/panel', 'render', ['title' => 'Status'])
* component('workspace/panel', ['render_call' => 'render'])
*/
function component($file_name, $prop = array())
{
Profiler::log('component '.$file_name, 1);
$caller_dir = component_caller_dir();
if($file_name == '')
return(component_error_banner('[component name empty]'));
$file_name = component_normalize_name($file_name);
// Default render method is 'render', but can be overridden
$prop['render_call'] = first($prop['render_call'] ?? false, 'render');
// Support for calling specific render methods: 'component:method'
if(stristr($file_name, ':')) // you can specify which render function to call
{
$prop['render_call'] = $file_name;
$file_name = nibble(':', $prop['render_call']);
}
// Get component definition from registry (load if not already loaded)
$renderer = $GLOBALS['render_funcs'][$file_name] ?? false;
if(!$renderer)
{
// Component not in registry - try to load from file
component_get_func($file_name, false, $caller_dir);
$renderer = $GLOBALS['render_funcs'][$file_name] ?? false;
}
// Generate unique ID for component instance if not provided
$prop['id'] = !empty($prop['id']) ? $prop['id'] : 'c'.($GLOBALS['id_counter']++);
$prop['filename'] = $file_name;
if(isset($renderer[$prop['render_call']]) && is_callable($renderer[$prop['render_call']]))
$result = ($renderer[$prop['render_call']]($prop, $renderer));
else if(isset($renderer[$prop['render_call']]))
$result = ($renderer[$prop['render_call']]);
else
$result = component_error_banner('component render method not found: '.$file_name.':'.$prop['render_call']);
Profiler::log(false, -1);
return($result);
}
+381
View File
@@ -0,0 +1,381 @@
<?php
class DB
{
static $dataCache = array();
static $link = false;
static $lastQuery = '';
static $keyDef = array();
static $affectedRows = 0;
static $writeOps = 0;
static $readOps = 0;
static $track_changes_in_tables = array();
static function isConnected()
{
return(self::$link instanceof mysqli);
}
static function connect()
{
if(self::$link) return;
if(!cfg('db/user')) die('database not configured');
Profiler::Log('DB::Connect() start');
self::$link = mysqli_connect(cfg('db/host'), cfg('db/user'), cfg('db/password'), cfg('db/database'), ini_get("mysqli.default_port"), cfg('db/socket')) or
critical('The database connection to server '.cfg('db/user').'@'.cfg('db/host').
' could not be established (code: '.@mysqli_connect_errno(self::$link).': '.@mysqli_connect_error(self::$link).')');
#if(mysqli_character_set_name(self::$link) != 'utf8')
mysqli_set_charset(self::$link, 'utf8mb4');
self::$track_changes_in_tables = cfg('track/changes');
Profiler::Log('DB::Connect() done');
}
static function Update($table, $searchCriteria, $updateFields)
{
self::$writeOps++;
DB::Query('UPDATE #'.DB::Safe($table).'
SET '.DB::MakeSetList($updateFields).'
WHERE '.DB::MakeSetList($searchCriteria, ' AND '));
}
static function GetCached($query, $parameters = null)
{
$cacheKey = 'dbq-'.md5($query.json_encode($parameters));
$result = Cache::Get($cacheKey);
if(!$result)
{
$result = self::Get($query, $parameters);
Cache::Set($cacheKey, $result);
}
return($result);
}
# get a list of datasets matching the $query
static function Get($query, $parameters = null, $keyByField = null)
{
self::$readOps++;
$result = array();
$query = self::ParseQueryParams($query, $parameters);
$lines = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' {query: '.$query.' }');
while ($line = mysqli_fetch_array($lines, MYSQLI_ASSOC))
{
if ($keyByField !== null && isset($line[$keyByField]))
$result[$line[$keyByField]] = $line;
else
$result[] = $line;
}
mysqli_free_result($lines);
Profiler::Log('DB::Get('.substr($query, 0, 40).'...)');
return $result;
}
# gets a list of keys for the table
static function Keys($tablename)
{
if(isset(self::$keyDef[$tablename]))
return(self::$keyDef[$tablename]);
self::$readOps++;
$result = array();
$sql = 'SHOW KEYS FROM `'.$tablename.'`';
$res = mysqli_query(self::$link, $sql) or critical('Cannot get keys // '.mysqli_error(self::$link));
while ($row = @mysqli_fetch_assoc($res))
{
if ($row['Key_name']=='PRIMARY')
array_push($result, $row['Column_name']);
}
Profiler::Log('DB::Keys('.$tablename.') REBUILD KEY CACHE');
self::$keyDef[$tablename] = $result;
return($result);
}
# get column info for $table
static function Info($table)
{
self::$readOps++;
$result = array('fields' => array(), 'info' => array());
foreach(self::Get('SHOW FULL COLUMNS FROM #'.$table) as $fld)
{
$ds = array();
foreach($fld as $k => $v)
{
$k = strtolower($k);
if($k == 'comment')
{
$p = explode(',', $v);
$v = false;
if(sizeof($p) > 0) foreach($p as $pi)
{
$pk = trim(nibble('=', $pi));
$ds['_'.$pk] = trim($pi);
}
}
if($v)
$ds[$k] = $v;
}
$ds['caption'] = first($ds['_title'], $ds['field']);
$result['fields'][$ds['field']] = $ds;
}
return($result);
}
# updates/creates the $dataset in the $tablename
static function Insert($tablename, $dataset)
{
self::$writeOps++;
$query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
') VALUES('.DB::MakeValuesList($dataset).')';
mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).'{ '.$query.' }');
self::$affectedRows += mysqli_affected_rows(self::$link);
return(mysqli_insert_id(self::$link));
}
# updates/creates the $dataset in the $tablename
static function Commit($tablename, &$dataset)
{
self::$writeOps++;
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
$keyvalue = $dataset[$keyname];
Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') start');
$cache_entry = $tablename.':'.$keyname.':'.$keyvalue;
unset(self::$dataCache[$cache_entry]);
$query='REPLACE INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
') VALUES('.DB::MakeValuesList($dataset).');';
# keeping this around just in case, but performance seems the same:
# $query='INSERT INTO '.$tablename.' ('.DB::MakeNamesList($dataset).
# ') VALUES('.DB::MakeValuesList($dataset).')
# ON DUPLICATE KEY UPDATE '.DB::MakeSetList($dataset).';';
mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { '.$query.' }');
$dataset[$keyname] = first($dataset[$keyname], mysqli_insert_id(self::$link));
self::$dataCache[$cache_entry] = $dataset;
Profiler::Log('DB::Commit('.$tablename.', '.$dataset[$keyname].') done');
return($dataset[$keyname]);
}
static function GetRowsMatch($table, $matchOptions, $fillIfEmpty = true)
{
self::$readOps++;
$where = array('1');
foreach($matchOptions as $k => $v)
$where[] = '('.$k.'="'.DB::Safe($v).'")';
$iwhere = implode(' AND ', $where);
$query = 'SELECT * FROM '.($table).
' WHERE '.$iwhere;
$resultDS = self::GetRowWithQuery($query);
if ($fillIfEmpty && sizeof($resultDS) == 0)
foreach($matchOptions as $k => $v)
$resultDS[$k] = $v;
Profiler::Log('DB::GetRowsMatch('.$table.') done');
return($resultDS);
}
# from table $tablename, get dataset with key $keyvalue
static function GetRow($tablename, $keyvalue, $keyname = '', $options = array())
{
if($keyvalue == '0') return(array());
$fields = @$options['fields'];
$fields = first($fields, '*');
if (!self::$link) return(array());
if ($keyname == '')
{
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
}
$cache_entry = $tablename.':'.$keyname.':'.$keyvalue;
if(isset(self::$dataCache[$cache_entry])) return(self::$dataCache[$cache_entry]);
$join = isset($options['join']) ? ' '.$options['join'] : '';
$query = 'SELECT '.$fields.' FROM '.$tablename.$join.' WHERE '.$keyname.'="'.DB::Safe($keyvalue).'";';
$queryResult = mysqli_query(self::$link, $query) or critical(mysqli_error(self::$link).' { Query: "'.$query.'" }');
self::$readOps++;
if ($line = @mysqli_fetch_array($queryResult, MYSQLI_ASSOC))
{
mysqli_free_result($queryResult);
self::$dataCache[$cache_entry] = $line;
Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.')');
return($line);
}
else
$result = array();
Profiler::Log('DB::GetRow('.$tablename.', '.$keyvalue.') #fail');
return($result);
}
static function RemoveRow($tablename, $keyvalue, $keyname = null)
{
if ($keyname == null)
{
$keynames = self::Keys($tablename);
$keyname = $keynames[0];
}
$res = (mysqli_query(self::$link, 'DELETE FROM '.$tablename.' WHERE '.$keyname.'="'.
DB::Safe($keyvalue).'";')
or critical(' Cannot remove dataset // '.mysqli_error(self::$link)));
Profiler::Log('DB::RemoveRow('.$tablename.', '.$keyvalue.') done');
self::$affectedRows += mysqli_affected_rows(self::$link);
self::$writeOps++;
return($res);
}
// retrieve dataset identified by SQL $query
static function GetRowWithQuery($query, $parameters = null)
{
$query = self::ParseQueryParams($query, $parameters);
$queryResult = mysqli_query(self::$link, $query);
if(!$queryResult)
return(critical('Error getting data // '.mysqli_error(self::$link).'{ '.$query.' }'));
if ($line = mysqli_fetch_array($queryResult, MYSQLI_ASSOC))
{
$result = $line;
mysqli_free_result($queryResult);
}
else
$result = array();
Profiler::Log('DB::GetRowWithQuery('.$query.')');
self::$readOps++;
return($result);
}
# execute a simple update $query
static function Query($query, $parameters = null)
{
$query = self::parseQueryParams($query, $parameters);
if (substr($query, -1, 1) == ';')
$query = substr($query, 0, -1);
$result = (mysqli_query(self::$link, $query)
or critical(' Query error // '.mysqli_error(self::$link)));
Profiler::Log('DB::Query('.$query.') done');
self::$affectedRows += mysqli_affected_rows(self::$link);
self::$writeOps++;
return($result);
}
# create a comma-separated list of keys in $dataset
static function MakeNamesList(&$dataset)
{
$result = '';
if (sizeof($dataset) > 0)
foreach (array_keys($dataset) as $k)
{
if ($k!='')
$result = $result.','.$k;
}
return(substr($result, 1));
}
# make a name-value list for UPDATE-queries
static function MakeValuesList(&$dataset)
{
$result = '';
if (sizeof($dataset) > 0)
foreach ($dataset as $k => $v)
{
if ($k!='')
$result = $result.',"'.DB::safe($v).'"';
}
return(substr($result, 1));
}
static function MakeSetList(&$dataset, $concat = ', ')
{
$result = array();
if (sizeof($dataset) > 0) foreach ($dataset as $k => $v)
{
if(substr($k, -1) == '+' || substr($k, -1) == '-')
{
$op = substr($k, -1);
$k = substr($k, 0, -1);
$result[] = $k.' = '.$k.' '.$op.' "'.DB::safe($v).'"';
}
else
{
$result[] = $k.' = "'.DB::safe($v).'"';
}
}
return(implode($concat, $result));
}
static function ParseQueryParams($query, $parameters = null)
{
if ($parameters != null)
{
$pctr = 0;
$result = '';
for($a = 0; $a < strlen($query); $a++)
{
$chr = substr($query, $a, 1);
if ($chr == '?')
{
$result .= '"'.DB::Safe($parameters[$pctr]).'"';
$pctr++;
}
else if ($chr == '&')
{
$result .= ''.intval($parameters[$pctr]).'';
$pctr++;
}
else if ($chr == ':')
{
$paramName = '';
$a += 1;
$pFormat = 'string';
if($query[$a] == ':')
{
$pFormat = 'number';
$a += 1;
}
while(!ctype_space($chr = substr($query, $a, 1)) && $a < strlen($query))
{
$paramName .= $chr;
$a += 1;
}
if($pFormat == 'number')
$result .= ' '.($parameters[$paramName]+0).' ';
else
$result .= ' "'.DB::Safe($parameters[$paramName]).'" ';
}
else
$result .= $chr;
}
}
else
$result = $query;
$q = str_replace('#', cfg('db/prefix'), $result);
self::$lastQuery = $q;
return($q);
}
static function Safe($raw)
{
if(!self::$link)
return(addslashes($raw));
else
return(mysqli_real_escape_string(self::$link, $raw));
}
}
DB::connect();
+144
View File
@@ -0,0 +1,144 @@
# db.class.php
A PHP database abstraction layer for MySQL/MariaDB with caching, parameter binding, and profiling.
## Connection
```php
DB::connect() // Auto-connects using config
DB::isConnected() // Check connection status
```
Uses configuration from `cfg('db/host')`, `cfg('db/user')`, etc.
## Basic Queries
```php
DB::Query($sql, $params) // Execute any SQL query
DB::Get($sql, $params) // Get multiple rows as array
DB::GetRowWithQuery($sql, $params) // Get single row
DB::GetCached($sql, $params) // Cached version of Get()
```
## CRUD Operations
### Reading Data
```php
DB::GetRow($table, $keyvalue) // Get row by primary key
DB::GetRow($table, $id, $keyname) // Get row by specific key
DB::GetRowsMatch($table, $criteria) // Get rows matching criteria
```
### Writing Data
```php
DB::Insert($table, $data) // Insert new row, returns ID
DB::Commit($table, $data) // Insert or update (REPLACE)
DB::Update($table, $where, $data) // Update existing rows
DB::RemoveRow($table, $keyvalue) // Delete row by key
```
## Parameter Binding
### Positional Parameters
```php
DB::Query('SELECT * FROM users WHERE id = ? AND name = ?', [$id, $name]);
DB::Query('SELECT * FROM users WHERE age > & AND active = ?', [$age, $active]);
```
### Named Parameters
```php
DB::Query('SELECT * FROM users WHERE name = :name', ['name' => $username]);
DB::Query('SELECT * FROM users WHERE age > ::age', ['age' => 25]); // Number format
```
Parameter formats:
- `?` - String (escaped)
- `&` - Number (unescaped integer)
- `:name` - Named string parameter
- `::name` - Named number parameter
## Table Information
```php
DB::Keys($table) // Get primary key column names
DB::Info($table) // Get full column information
```
## Utility Methods
```php
DB::Safe($string) // Escape string for SQL
DB::MakeNamesList($array) // Create column list for INSERT
DB::MakeValuesList($array) // Create values list for INSERT
DB::MakeSetList($array, $separator) // Create SET clause for UPDATE
```
## Advanced Features
### Table Prefix Support
```php
DB::Query('SELECT * FROM #users') // # replaced with cfg('db/prefix')
```
### Increment/Decrement Operations
```php
DB::Update('users', ['id' => 1], ['score+' => 10]); // score = score + 10
DB::Update('users', ['id' => 1], ['lives-' => 1]); // lives = lives - 1
```
### Caching
```php
$users = DB::GetCached('SELECT * FROM users WHERE active = 1');
// Subsequent calls return cached results
```
### Statistics
```php
DB::$affectedRows // Rows affected by last operation
DB::$readOps // Count of read operations
DB::$writeOps // Count of write operations
DB::$lastQuery // Last executed query
```
## Examples
### Basic Usage
```php
// Insert new user
$userId = DB::Insert('users', [
'name' => 'John Doe',
'email' => 'john@example.com',
'active' => 1
]);
// Get user by ID
$user = DB::GetRow('users', $userId);
// Update user
DB::Update('users', ['id' => $userId], ['last_login' => date('Y-m-d H:i:s')]);
// Find users
$activeUsers = DB::Get('SELECT * FROM users WHERE active = ?', [1]);
```
### Complex Queries
```php
// Named parameters
$results = DB::Get('
SELECT * FROM #posts
WHERE author_id = :author
AND created_date > :date
AND status = :status
', [
'author' => $authorId,
'date' => '2023-01-01',
'status' => 'published'
]);
// Mixed parameter types
DB::Query('UPDATE #users SET score = score + ::points WHERE id = :id', [
'points' => 100, // Number (unescaped)
'id' => $userId // String (escaped)
]);
```
@@ -0,0 +1,217 @@
<?php
class Filebase
{
static function hash($s = false)
{
if($s === false) $s = time().random_int(0, 2147483647);
$s = strtolower(substr(trim($s), 0, 64));
return(substr(base_convert_ex(
sha1(sha1('qw0e983124o521öl34u9087'.$s)),
'0123456789abcdef',
'0123456789abcdefghijklmnopqrstuvwxyz'
), -10));
}
static function make_bucket_path($p)
{
if(stristr($p, '/') !== false)
{
$seg = explode('/', $p);
$p = array_shift($seg);
return(substr($p, -2).'/'.$p.'/'.implode('/', $seg));
}
return(substr($p, -2).'/'.$p);
}
static function write_file($filename, $data)
{
$fp = fopen($filename, "c+");
if(!$fp)
{
Log::text('<ERR>', 'file_put_contents_ex('.$filename.') could not write file');
return;
}
if (flock($fp, LOCK_EX))
{
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, $data);
}
else
{
Log::text('<ERR>', 'file_put_contents_ex('.$filename.') could not acquire lock');
}
fclose($fp);
}
static function delete_file($file_name)
{
unlink($file_name);
}
static function read_file($file_name)
{
$fsz = filesize($file_name);
if($fsz == 0)
return('');
$fp = fopen($file_name, "rb+");
if(!$fp)
return('');
if (flock($fp, LOCK_SH)) {
$content = fread($fp, $fsz);
flock($fp, LOCK_UN);
} else {
Log::text('<ERR>', 'read_file('.$file_name.') could not acquire lock');
}
fclose($fp);
return($content);
}
static function write_data($class, $bucket, $type, $data)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true);
$fn = $storage_path.'/'.$type.'.json';
self::write_file($fn, json_encode($data));
}
static function read_data($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
$fn = $storage_path.'/'.$type.'.json';
return(json_decode(self::read_file($fn), true));
}
static function delete_data($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(self::delete_file($storage_path.'/'.$type.'.json'));
}
static function get_data_filename($class, $bucket, $type)
{
return(first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket).'/'.$type.'.json');
}
static function list_bucket($class, $bucket)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
foreach(explode(chr(10), trim(shell_exec('ls -1 '.escapeshellarg($storage_path)))) as $name)
{
if(substr($name, 0, 1) != '_' && trim($name) != '')
$items[] = $name;
}
return($items);
}
static function search_bucket($class, $bucket, $q)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
foreach(explode(chr(10), trim(shell_exec('grep -irlF '.escapeshellarg($q).' '.escapeshellarg($storage_path)))) as $l)
{
$name = substr($l, strlen($storage_path)+1, -5);
if(substr($name, 0, 1) != '_' && trim($name) != '')
$items[] = $name;
}
return($items);
}
static function delete_bucket($class, $bucket)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(stristr($storage_path, '*') !== false) return;
if(stristr($storage_path, '?') !== false) return;
$result = trim(shell_exec('rm -r '.escapeshellarg($storage_path).' 2>&1'));
return($result);
}
static function write_log($class, $bucket, $type, $data)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
if(!file_exists($storage_path)) @mkdir($storage_path, 0774, true);
WriteToFile($storage_path.'/'.$type.'.log', json_encode($data).chr(10));
}
static function read_log($class, $bucket, $type, $line_count = 8, $offset = false)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(get_json_tail($storage_path.'/'.$type.'.log', $line_count, $offset));
}
static function line_count($class, $bucket, $type)
{
$storage_path = first($GLOBALS['config']['filebase']['path'], 'data/').$class.'/'.self::make_bucket_path($bucket);
return(intval(trim(shell_exec('wc -l '.escapeshellarg($storage_path.'/'.$type.'.log')))));
}
static function get_json_tail($from_file, $line_count = 8, $offset = false)
{
if($offset > 0)
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count)));
}
else
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file)));
}
return(json_lines($lines));
}
static function get_tail($from_file, $line_count = 8, $offset = false)
{
$line_count = intval($line_count);
if($offset > 0)
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($offset+$line_count).' '.escapeshellarg($from_file).' | head -n '.escapeshellarg($line_count)));
}
else
{
$lines = trim(shell_exec(
'tail -n '.escapeshellarg($line_count).' '.escapeshellarg($from_file)));
}
return(explode(chr(10), $lines));
}
static function get_all_lines($from_file)
{
return(explode(chr(10), trim(file_get_contents($from_file))));
}
static function truncate_log($from_file, $lines = 128)
{
$lines = intval($lines);
$tmp = tempnam(sys_get_temp_dir(), 'trunc_');
shell_exec('tail -n '.$lines.' '.escapeshellarg($from_file).' > '.escapeshellarg($tmp).' && cp '.escapeshellarg($tmp).' '.escapeshellarg($from_file));
@unlink($tmp);
return true;
}
static function json_lines($lines)
{
if($lines == '')
{
return(array());
}
else
{
$result = array();
foreach(explode(chr(10), $lines) as $line)
$result[] = json_decode($line, true);
return($result);
}
}
}
@@ -0,0 +1,111 @@
<?php
class HTTP
{
/**
* Make a POST request with JSON or form data
*/
public static function post($url, $data = [], $headers = [])
{
$ch = curl_init();
$default_headers = [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
'User-Agent: Web-App-Starter/1.0'
];
$headers = array_merge($default_headers, $headers);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => is_array($data) ? http_build_query($data) : $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'cURL Error: ' . $error, 'http_code' => 0];
}
$decoded = json_decode($response, true);
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'raw' => $response,
'data' => $decoded ?: $response,
'error' => $http_code >= 400 ? "HTTP Error $http_code" : null
];
}
/**
* Make a GET request
*/
public static function get($url, $params = [], $headers = [])
{
if (!empty($params)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params);
}
$ch = curl_init();
$default_headers = [
'Accept: application/json',
'User-Agent: Web-App-Starter/1.0'
];
$headers = array_merge($default_headers, $headers);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'cURL Error: ' . $error, 'http_code' => 0];
}
$decoded = json_decode($response, true);
return [
'success' => $http_code >= 200 && $http_code < 300,
'http_code' => $http_code,
'raw' => $response,
'data' => $decoded ?: $response,
'error' => $http_code >= 400 ? "HTTP Error $http_code" : null
];
}
/**
* Make a request with Bearer token authentication
*/
public static function get_with_token($url, $token, $params = [])
{
return self::get($url, $params, [
'Authorization: Bearer ' . $token
]);
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
class Log
{
static $app_name = 'webappstarter';
static function make($module, $text)
{
if(is_array($text))
$text = str_replace(array('"', '\\'), '', json_encode($text));
$seg = array();
$seg[] = first($_SESSION['username'] ?? false, 'anonymous');
$seg[] = first($_SERVER['REMOTE_ADDR'] ?? false, 'cli');
$seg[] = round(memory_get_peak_usage()/1024).'kB';
$seg[] = round(Profiler::get_time()).'ms';
$seg[] = $module;
$seg[] = $text;
return($seg);
}
static function audit($module, $text = '', $class = 'warning')
{
$seg = self::make($module, $text);
shell_exec('echo '.escapeshellarg(implode('|', $seg)).' | systemd-cat -t '.(self::$app_name).' -p '.escapeshellarg($class));
}
static function debug($module, $text)
{
write_to_file('log/debug.'.gmdate('Y-m').'.log',
implode(chr(9), self::make($module, $text)).chr(10));
}
static function text($module, $text)
{
write_to_file('log/log.'.gmdate('Y-m').'.log',
implode(chr(9), self::make($module, $text)).chr(10));
}
}
+468
View File
@@ -0,0 +1,468 @@
<?php
/**
* ODT Template Processing and PDF Generation Class
*
* This class provides functionality to process OpenDocument Text (ODT) templates
* with placeholder substitution and automatic PDF conversion using LibreOffice.
*
* REQUIREMENTS:
* - PHP with shell_exec() enabled
* - unzip command available in system PATH
* - LibreOffice (soffice) installed for PDF conversion
*
* BASIC USAGE:
*
* 1. Create an ODT template file with placeholders
* 2. Initialize the ODT class with template path
* 3. Prepare the template (extracts and loads content)
* 4. Replace placeholders with your data
* 5. Generate output ODT and PDF files
*
* EXAMPLE:
*
* $odt = new ODT('/path/to/template.odt');
*
* if ($odt->prepare()) {
* $data = [
* 'customer_name' => 'John Doe',
* 'invoice_date' => time(),
* 'total_amount' => 1250.50,
* 'items' => [
* ['name' => 'Product A', 'price' => 500.00, 'qty' => 1],
* ['name' => 'Product B', 'price' => 750.50, 'qty' => 1]
* ]
* ];
*
* $odt->replace_placeholders($data);
* $pdf_path = $odt->create_output('/path/to/output.odt');
*
* if ($pdf_path) {
* echo "PDF created: " . $pdf_path;
* } else {
* echo "Error: " . $odt->error_msg;
* }
* } else {
* echo "Error: " . $odt->error_msg;
* }
*
* PLACEHOLDER SYNTAX:
*
* In your ODT template, use LibreOffice placeholders (Insert > Field > Other > Variables > User Field)
* or direct placeholder markup:
*
* Simple placeholders:
* <text:placeholder>customer_name</text:placeholder>
*
* Formatted placeholders with JSON properties in description:
* <text:placeholder text:description='{"format":"date"}'>invoice_date</text:placeholder>
* <text:placeholder text:description='{"format":"currency","decimals":2}'>total_amount</text:placeholder>
*
* SUPPORTED FORMATS:
*
* - "date" - Formats timestamp as date (uses cfg('date_format') or custom template)
* - "time" - Formats timestamp as time (uses cfg('time_format') or custom template)
* - "datetime" - Formats timestamp as datetime (uses cfg('datetime_format') or custom template)
* - "currency" - Formats number as currency with € symbol
* - "number" - Formats number with specified decimal places
* - "duration" - Converts seconds to "Xh Ymin" format
*
* FORMAT OPTIONS:
*
* - "template": Custom date/time format string (e.g., "Y-m-d H:i:s")
* - "decimals": Number of decimal places for currency/number formatting
* - "default": Default value if placeholder data is empty
* - "emptyifzero": Show empty string if numeric value is zero
* - "delsegmentifzero": Delete entire segment if numeric value is zero
* - "newline": Add newlines "before", "after", or "before,after" the value
*
* LISTS AND TABLES:
*
* For repeating table rows, use a special placeholder in a table row:
* <text:placeholder text:placeholder-type="table">items</text:placeholder>
*
* This will repeat the table row for each item in the 'items' array.
* Within the repeated row, access item properties with 'item.' prefix:
* <text:placeholder>item.name</text:placeholder>
* <text:placeholder>item.price</text:placeholder>
*
* The class automatically calculates sums for numeric fields:
* <text:placeholder>sums.items.price</text:placeholder>
*
* ERROR HANDLING:
*
* Always check the return values and $odt->error_msg for error details:
* - prepare() returns false on failure
* - create_output() returns false on failure, PDF path on success
* - Check $odt->error_msg for specific error messages
*
* PROPERTIES:
*
* - $template_name: Path to the ODT template file
* - $content_xml: Loaded and processed content.xml from ODT
* - $temp_dir: Temporary directory for ODT extraction
* - $error_msg: Last error message
* - $odt_filename: Path to generated ODT file
* - $pdf_filename: Path to generated PDF file
* - $debug_output: Debug output from shell commands
*
* NOTES:
*
* - Temporary files are automatically cleaned up in destructor
* - PDF conversion requires LibreOffice to be installed and accessible
* - The class assumes UTF-8 encoding for all text content
* - Newlines in data are converted to ODT line breaks
* - All string values are properly escaped for XML
*/
class ODT
{
public $template_name;
public $content_xml;
public $temp_dir;
public $error_msg;
public $odt_filename;
public $pdf_filename;
public $debug_output;
function __construct($template_name)
{
$this->template_name = $template_name;
if(!file_exists($this->template_name)) {
$this->error_msg = 'Template not found: '.$template_name;
}
$this->temp_dir = '/tmp/'.uniqid();
}
/**
* Check if all required command line tools are available and working
* @return array Array with 'status' (bool) and 'messages' (array of status messages)
*/
static function check_requirements()
{
$results = [
'status' => true,
'messages' => []
];
$unzip_check = shell_exec('which unzip 2>/dev/null');
if (empty(trim($unzip_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: unzip command not found in PATH';
} else {
$results['messages'][] = 'OK: unzip found at ' . trim($unzip_check);
$unzip_version = shell_exec('unzip -v 2>&1 | head -1');
if ($unzip_version) {
$results['messages'][] = 'INFO: ' . trim($unzip_version);
}
}
$zip_check = shell_exec('which zip 2>/dev/null');
if (empty(trim($zip_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: zip command not found in PATH';
} else {
$results['messages'][] = 'OK: zip found at ' . trim($zip_check);
}
$soffice_check = shell_exec('which soffice 2>/dev/null') || '';
if (empty(trim($soffice_check))) {
$results['status'] = false;
$results['messages'][] = 'ERROR: LibreOffice (soffice) not found in PATH';
} else {
$results['messages'][] = 'OK: LibreOffice found at ' . trim($soffice_check);
$soffice_version = shell_exec('soffice --version 2>&1');
if ($soffice_version) {
$results['messages'][] = 'INFO: ' . trim($soffice_version);
}
$headless_test = shell_exec('timeout 10 soffice --headless --help 2>&1') || '';
if (strpos($headless_test, 'headless') !== false || strpos($headless_test, 'convert-to') !== false) {
$results['messages'][] = 'OK: LibreOffice headless mode is working';
} else {
$results['status'] = false;
$results['messages'][] = 'ERROR: LibreOffice headless mode test failed';
$results['messages'][] = 'DEBUG: ' . trim($headless_test);
}
}
if (!is_dir('/tmp')) {
$results['status'] = false;
$results['messages'][] = 'ERROR: /tmp directory does not exist';
} elseif (!is_writable('/tmp')) {
$results['status'] = false;
$results['messages'][] = 'ERROR: /tmp directory is not writable';
} else {
$results['messages'][] = 'OK: /tmp directory exists and is writable';
}
$test_temp_dir = '/tmp/odt_test_' . uniqid();
if (!mkdir($test_temp_dir, 0777, true)) {
$results['status'] = false;
$results['messages'][] = 'ERROR: Cannot create temporary directories in /tmp';
} else {
$results['messages'][] = 'OK: Can create temporary directories';
rmdir($test_temp_dir);
}
return $results;
}
static function parse_attributes($tag) {
$attributes = [];
while($tag != '')
{
$a_name = nibble('="', $tag);
$a_value = nibble('"', $tag);
$attributes[trim($a_name)] = trim($a_value);
}
return $attributes;
}
function prepare()
{
// Create the temporary directory
if (!mkdir($this->temp_dir, 0777, true)) {
$this->error_msg = 'Failed to create temp directory';
return false;
}
// Unzip the template into the temp directory using shell_exec()
$command = 'unzip ' . escapeshellarg($this->template_name) . ' -d ' . escapeshellarg($this->temp_dir) . ' 2>&1';
$output = shell_exec($command);
if ($output === null) {
$this->error_msg = 'Failed to unzip the template';
return false;
}
// Load content.xml into memory
$content_file = $this->temp_dir . '/content.xml';
if (!file_exists($content_file)) {
$this->error_msg = 'content.xml not found';
return false;
}
$this->content_xml = file_get_contents($content_file);
if ($this->content_xml === false) {
$this->error_msg = 'Failed to read content.xml';
return false;
}
return true;
}
function odt_entities($raw)
{
return str_replace(chr(10), '<text:line-break/>', safe($raw, ENT_XML1 | ENT_QUOTES, 'UTF-8'));
}
function break_into_segments($s)
{
$segment_handlers = [
'<table:table-row ' => function(&$cake, &$seg) {
$rest_of_row = nibble('</table:table-row>', $cake);
# check whether this segment contains an items marker indicating we should
# iterate this segment over a list in the data
$items_marker = '<text:placeholder text:placeholder-type="table">';
if(strpos($rest_of_row, $items_marker) !== false)
{
$r0 = nibble($items_marker, $rest_of_row);
$items_param = trim(str_replace(['&lt;', '&gt;'], '',
nibble('</text:placeholder>', $rest_of_row)));
$seg[] = [
'type' => 'list',
'content' => '<table:table-row '.$r0.$rest_of_row.'</table:table-row>',
'fields' => $items_param];
}
else $seg[] = [
'type' => 'flat',
'content' => '<table:table-row '.$rest_of_row.'</table:table-row>'];
},
];
$seg = [];
while($s != '')
{
$seg_start_found = false;
$sc = nibble(array_keys($segment_handlers), $s, $seg_start_found);
$seg[] = ['type' => 'flat', 'content' => $sc];
if($seg_start_found !== false)
{
$segment_handlers[$seg_start_found]($s, $seg);
}
}
return($seg);
}
function run_segment($seg, $params)
{
$result = '';
$sc = $seg['content'];
while($sc != '')
{
$placeholder_found = false;
$result .= nibble('<text:placeholder', $sc, $placeholder_found);
if($placeholder_found)
{
$p_attributes = ODT::parse_attributes(nibble('>', $sc));
$p_prop = [];
if(isset($p_attributes['text:description']) && $p_attributes['text:description'])
{
if(substr($p_attributes['text:description'], 0, 1) == '{')
{
$decoded = json_decode(
str_replace('&quot;', '"', $p_attributes['text:description']), true);
if($decoded !== null) {
$p_prop = $decoded;
}
}
else
{
$p_prop['format'] = $p_attributes['text:description'];
}
}
$p_content = str_replace(array('&gt;', '&lt;'), '',
nibble('</text:placeholder>', $sc));
$placeholder_key = $p_content;
$value = first($params[$placeholder_key] ?? null, $p_prop['default'] ?? null, '');
switch($p_prop['format'] ?? '')
{
case('date'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('date_format')), intval($value));
} break;
case('time'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('time_format')), intval($value));
} break;
case('datetime'):
{
if($value == 0) $value = '-'; else
$value = date(first($p_prop['template'], cfg('datetime_format')), intval($value));
} break;
case('currency'):
{
$value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '').' €';
} break;
case('number'):
{
$value = number_format(floatval($value), first($p_prop['decimals'] ?? null, 2), ',', '');
} break;
case('duration'):
{
$value = intval($value);
$h = floor($value/3600); $value -= $h*3600;
$m = floor($value/60); $value -= $m*60;
$s = $value;
$value = '';
if($h > 0) $value .= $h.'h ';
$value .= $m.'min';
} break;
}
if(($p_prop['emptyifzero'] ?? false) && floatval($value) == 0)
$value = '';
if(($p_prop['delsegmentifzero'] ?? false) && floatval($value) == 0)
return('');
if(!is_string($value))
$value = trim($value);
if(($p_prop['newline'] ?? false) && $value != '')
{
if(str_contains($p_prop['newline'], 'before'))
$value = chr(10).$value;
if(str_contains($p_prop['newline'], 'after'))
$value = $value.chr(10);
}
if(!is_array($value))
$result .= $this->odt_entities($value);
}
}
return $result;
}
function replace_placeholders(&$params)
{
$result = '';
foreach($this->break_into_segments($this->content_xml) as $seg)
{
if($seg['type'] == 'list')
{
if(isset($params[$seg['fields']]) && is_array($params[$seg['fields']])) {
foreach($params[$seg['fields']] as $item)
{
$np = $params;
foreach($item as $k => $v)
{
$np['item.'.$k] = $v;
$params['sums.'.$seg['fields'].'.'.$k] += floatval($v);
}
$result .= $this->run_segment($seg, $np);
}
}
}
else
{
$result .= $this->run_segment($seg, $params);
}
}
return $this->content_xml = $result;
}
function create_output($out_filename)
{
$this->odt_filename = $out_filename;
$content_file = $this->temp_dir . '/content.xml';
if (file_put_contents($content_file, $this->content_xml) === false) {
$this->error_msg = 'Failed to write content.xml';
return false; //
}
$this->debug_output = trim(
shell_exec('cd '.escapeshellarg($this->temp_dir).' ; zip -r ../tmp.odt * 2>&1'));
$zip_output = dirname($this->temp_dir).'/tmp.odt';
if(!file_exists($zip_output))
{
$this->error_msg = 'Failed to create tmp ODT file (shell "'.$this->debug_output.'")';
return false;
}
if(file_exists($out_filename)) {
unlink($out_filename);
}
shell_exec('mv '.escapeshellarg($zip_output).' '.escapeshellarg($out_filename));
if (!file_exists($out_filename)) {
$this->error_msg = 'Failed to create ODT file "'.$out_filename.'"';
return false; //
}
$this->pdf_filename = $pdf_output = pathinfo($out_filename, PATHINFO_DIRNAME) . '/' . pathinfo($out_filename, PATHINFO_FILENAME) . '.pdf';
$command = 'HOME=/tmp ; soffice --headless --convert-to pdf ' . escapeshellarg($out_filename) . ' --outdir ' . escapeshellarg(pathinfo($out_filename, PATHINFO_DIRNAME)) . ' 2>&1';
$this->debug_output = shell_exec($command);
if (!file_exists($pdf_output)) {
$this->error_msg = 'PDF conversion failed';
return false; //
}
return $pdf_output;
}
function __destruct()
{
$this->delete_directory($this->temp_dir);
}
function delete_directory($dir)
{
if (!is_dir($dir)) return;
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$path = "$dir/$item";
is_dir($path) ? $this->delete_directory($path) : unlink($path);
}
rmdir($dir);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
class Profiler
{
static $log = array();
static $start = 0;
static $last = 0;
static $current = 0;
static $indent_str = '';
static $indent_level = 0;
# makes a commented profiler entry
static function log($text, $indent_delta = 0)
{
$thistime = microtime(true);
$absoluteMS = $thistime - self::$start;
self::$indent_level += $indent_delta;
if($indent_delta < 0) // if less than zero, update indent before logging
self::$indent_str = str_repeat(' ', self::$indent_level * 2);
if($text) self::$log[] =
number_format($absoluteMS*1000, 3).'ms | '.
number_format(1000*($thistime - self::$last), 3).'ms | '.
ceil(memory_get_usage()/1024).' kB | '.self::$indent_str.$text;
if($indent_delta > 0) // if greater than zero, update indent after logging
self::$indent_str = str_repeat(' ', self::$indent_level * 2);
self::$last = $thistime;
self::$current = $absoluteMS;
return($thistime);
}
static function get_time()
{
return(1000*(microtime(true) - self::$start));
}
static function start()
{
self::$start = self::$last = microtime(true);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
class SVG
{
static function circle_segment($midx, $midy, $radius, $start_angle, $end_angle,
$stroke_color = 'rgba(120,120,120,1.0)', $stroke_width = 1, $fill_color = 'rgba(0,0,0,0)', $style = '')
{
$start_x = $midx + $radius * cos($start_angle);
$start_y = $midy + $radius * sin($start_angle);
$end_x = $midx + $radius * cos($end_angle);
$end_y = $midy + $radius * sin($end_angle);
while ($start_angle < 0) $start_angle += 2 * M_PI;
while ($end_angle < 0) $end_angle += 2 * M_PI;
while ($start_angle >= 2 * M_PI) $start_angle -= 2 * M_PI;
while ($end_angle >= 2 * M_PI) $end_angle -= 2 * M_PI;
$angle_diff = $end_angle - $start_angle;
if ($angle_diff < 0) $angle_diff += 2 * M_PI;
$large_arc_flag = ($angle_diff > M_PI) ? 1 : 0;
$sweep_flag = 1;
?>
<path d="M <?= $start_x ?> <?= $start_y ?> A <?= $radius ?> <?= $radius ?> 0 <?= $large_arc_flag ?> <?= $sweep_flag ?> <?= $end_x ?> <?= $end_y ?>"
style="<?= $style ?>" fill="<?= $fill_color ?>" stroke="<?= $stroke_color ?>" stroke-width="<?= $stroke_width ?>" />
<?php
}
}
@@ -0,0 +1,157 @@
<?php
function theme_option($key, $default = null)
{
$themes = cfg('theme/options');
$current_theme_key = (string)cfg('theme/key');
$current_theme = is_array($themes) && isset($themes[$current_theme_key]) ? $themes[$current_theme_key] : array();
return array_key_exists($key, $current_theme) ? $current_theme[$key] : $default;
}
function theme_html_class($extra_class = '')
{
$classes = array('no-js');
if(cfg('theme/mode') === 'dark')
$classes[] = 'dark-theme';
if($extra_class !== '')
$classes[] = $extra_class;
return trim(implode(' ', $classes));
}
function theme_render_head($overrides = array())
{
$theme_path = (string)cfg('theme/path');
$theme_path_common = 'themes/common/';
$description = first($overrides['description'] ?? false, theme_option('meta_description', ''), 'Web App Starter theme');
$theme_color = first($overrides['theme_color'] ?? false, theme_option('theme_color', ''), '#0f172a');
$apple_icon = url_root().ltrim($theme_path, '/').'icon.png';
?>
<meta charset="utf-8">
<title><?= first(URL::$route['page-title'] ?? false, cfg('site/default_page_title')).' | '.cfg('site/name') ?></title>
<meta name="description" content="<?= asafe((string)$description) ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="<?= asafe((string)$theme_color) ?>">
<link rel="apple-touch-icon" href="<?= $apple_icon ?>">
<link rel="icon" type="image/png" sizes="32x32" href="<?= $apple_icon ?>">
<?php
include_css($theme_path.'css/style.css');
include_css($theme_path_common.'fontawesome/css/all.min.css');
include_js('js/u-query.js');
include_js('js/morphdom.js');
include_js('js/site.js');
foreach((array)($overrides['extra_css'] ?? array()) as $extra_css)
include_css($extra_css);
foreach((array)($overrides['extra_js'] ?? array()) as $extra_js)
include_js($extra_js);
}
function theme_render_global_controls($embed_mode, $options = array())
{
if($embed_mode)
return;
$show_theme_switcher = array_key_exists('theme_switcher', $options) ? (bool)$options['theme_switcher'] : true;
$show_cookie_consent = array_key_exists('cookie_consent', $options) ? (bool)$options['cookie_consent'] : true;
if($show_theme_switcher)
echo component('components/example/theme-switcher');
if($show_cookie_consent)
echo component('components/basic/cookie-consent');
}
function theme_menu_href($menu_key, $menu_item)
{
if(!empty($menu_item['external']))
return '/'.ltrim((string)$menu_key, '/');
return URL::Link((string)$menu_key);
}
function theme_get_user_context()
{
require_once __DIR__.'/user.class.php';
$signed_in = User::IsSignedIn();
$profile = $signed_in ? (User::$current_profile ?? array()) : array();
return array(
'signed_in' => $signed_in,
'profile' => $profile,
'avatar' => first($profile['avatar_url'] ?? false, $profile['profile_image'] ?? false),
'display_name' => first($profile['username'] ?? false, $profile['email'] ?? false, 'Account'),
);
}
function theme_render_account_links($options = array())
{
$context = theme_get_user_context();
$wrapper_class = trim((string)first($options['wrapper_class'] ?? false, 'nav-account'));
$name_class = trim((string)first($options['name_class'] ?? false, 'account-name'));
$links_wrapper_class = trim((string)($options['links_wrapper_class'] ?? ''));
$show_avatar = !empty($options['show_avatar']);
$show_name = array_key_exists('show_name', $options) ? (bool)$options['show_name'] : true;
?><div class="<?= asafe($wrapper_class) ?>"><?php
if($context['signed_in'])
{
if($show_avatar && $context['avatar'])
{
?><img src="<?= safe($context['avatar']) ?>" alt="Profile" class="nav-avatar" onerror="this.style.display='none';"><?php
}
if($show_name)
{
?><span class="<?= asafe($name_class) ?>"><?= safe((string)$context['display_name']) ?></span><?php
}
if($links_wrapper_class !== '')
?><div class="<?= asafe($links_wrapper_class) ?>"><?php
?><a href="<?= URL::Link('account/profile') ?>">Profile</a>
<a href="<?= URL::Link('account/logout') ?>">Logout</a><?php
if($links_wrapper_class !== '')
?></div><?php
}
else
{
if($links_wrapper_class !== '')
?><div class="<?= asafe($links_wrapper_class) ?>"><?php
?><a href="<?= URL::Link('account/login') ?>">Login</a><?php
if(cfg('users/enable_signup'))
{
?><a href="<?= URL::Link('account/register') ?>">Register</a><?php
}
if($links_wrapper_class !== '')
?></div><?php
}
?></div><?php
}
function theme_render_standard_nav($options = array())
{
$nav_class = trim((string)($options['nav_class'] ?? ''));
$account_wrapper_class = trim((string)first($options['account_wrapper_class'] ?? false, 'nav-account'));
$show_avatar = array_key_exists('show_avatar', $options) ? (bool)$options['show_avatar'] : true;
?><nav<?= $nav_class !== '' ? ' class="'.asafe($nav_class).'"' : '' ?>>
<div class="nav-menu">
<a href="<?= URL::Link('') ?>"><?= cfg('site/name') ?></a>
<?php foreach((array)cfg('menu') as $menu_key => $menu_item) if(empty($menu_item['hidden'])) { ?>
<a href="<?= theme_menu_href($menu_key, $menu_item) ?>"><?= safe($menu_item['title']) ?></a>
<?php } ?>
</div>
<?php theme_render_account_links(array(
'wrapper_class' => $account_wrapper_class,
'name_class' => 'account-name',
'show_avatar' => $show_avatar,
)); ?>
</nav><?php
}
function theme_render_footer($text = null, $options = array())
{
if(!empty($options['embed_mode']))
return;
$footer_text = first($text, theme_option('footer_text', false), cfg('site/name'));
$inner_class = trim((string)($options['inner_class'] ?? ''));
?><footer><?php
if($inner_class !== '')
{
?><div class="<?= asafe($inner_class) ?>"><p><?= safe((string)$footer_text) ?></p></div><?php
}
else
{
?><div style="max-width: 1200px; margin: 0 auto; padding: 0 1rem;"><p><?= safe((string)$footer_text) ?></p></div><?php
}
?></footer><?php
}
+113
View File
@@ -0,0 +1,113 @@
# ulib.php
A collection of PHP utility functions for common web development tasks.
## Autoloading & Setup
The library automatically registers a class autoloader and starts the profiler:
```php
// Auto-loads classes from lib/{classname}.class.php
require_once 'lib/ulib.php';
```
## Asset Management
```php
include_js('js/script.js') // Outputs <script> tag with cache busting
include_css('css/style.css') // Outputs <link> tag with cache busting
get_file_location($file) // Find file in include paths
```
Assets are automatically versioned with `filemtime()` for cache busting.
## HTML/Security Functions
```php
safe($text) // HTML escape for content
asafe($text) // HTML escape for attributes (strips newlines)
jsafe($data) // JSON encode (alias for json_encode)
```
## Configuration
```php
cfg('database/host') // Get config value with slash notation
cfg('url/root') // Access nested config arrays
```
Reads from `$GLOBALS['config']` with support for nested keys using `/` separator.
## Utility Functions
```php
first($a, $b, $c) // Return first non-empty value
alnum($text, '_', true) // Keep only alphanumeric + spaces
write_to_file($file, $content) // Append content to file
```
### first() Examples
```php
$name = first($user_input, $default_name, 'Anonymous');
$config = first($_GET['theme'], $_COOKIE['theme'], 'default');
```
## String Processing
### String Parsing
```php
nibble(':', $path, $found) // Extract substring before delimiter
// Example: nibble(':', 'user:pass', $found) → 'user', $path becomes 'pass'
```
### Date/Time
```php
age_to_string($timestamp) // Human-friendly time differences
// Examples: "just now", "5 min ago", "2 h ago", "Mon 14:30"
```
### Hash Generation
```php
make_hash() // Generate random hash (10 chars)
make_hash($input, 20) // Hash specific input (20 chars)
```
### Base Conversion
```php
base_convert_any($num, $from, $to) // Convert between any number bases
// Example: base_convert_any('FF', '0123456789ABCDEF', '0123456789') → '255'
```
## Advanced Features
- **Include Path Resolution**: Searches multiple paths for files
- **Automatic Permissions**: Sets 0777 on written files
- **Cache Busting**: Automatic versioning for JS/CSS
- **Reference Parameters**: Functions like `nibble()` modify input variables
- **Flexible Base Conversion**: Support for custom character sets
## Common Usage Patterns
```php
// Configuration-driven asset loading
include_css(cfg('theme/css_file'));
// Safe template output
echo '<div title="' . asafe($user_input) . '">' . safe($content) . '</div>';
// Fallback values
$title = first($_POST['title'], $default_title, 'Untitled');
// URL parsing
$protocol = nibble('://', $url); // Extract 'http' from 'http://example.com'
// Time display
echo 'Posted ' . age_to_string($post_timestamp);
```
## Error Handling
Functions include built-in error handling:
- `get_file_location()` dies with error message if file not found
- `write_to_file()` uses error suppression for chmod
- Most functions return safe defaults for invalid input
+254
View File
@@ -0,0 +1,254 @@
<?php
spl_autoload_register(function ($class_name) {
$classFile = 'lib/'.strtolower($class_name).'.class.php';
if(file_exists($classFile))
{
include($classFile);
return;
}
});
Profiler::start();
function url_root()
{
$root = (string)cfg('url/root');
if($root !== '' && substr($root, -1) !== '/')
$root .= '/';
return $root;
}
function get_file_location($file, $error_if_not_found = true)
{
$file = ltrim((string)$file, '/');
if(str_contains($file, '..'))
{
if($error_if_not_found)
die('invalid file path: '.$file);
return false;
}
if(file_exists($file)) return $file;
foreach((array)$GLOBALS['config']['site']['include_paths'] as $path)
{
$path = rtrim((string)$path, '/').'/';
if(file_exists($path.$file)) return $path.$file;
}
if($error_if_not_found)
die('file not found: '.$file);
return false;
}
function asset_already_included($file_location)
{
static $included_assets = array();
if(isset($included_assets[$file_location]))
return true;
$included_assets[$file_location] = true;
return false;
}
function include_js($src_file)
{
if(!($file_location = get_file_location($src_file))) return;
if(asset_already_included($file_location)) return;
?><script src="<?= url_root().$file_location ?>?v=<?= filemtime($file_location) ?>"></script><?php
}
function include_css($src_file)
{
if(!($file_location = get_file_location($src_file))) return;
if(asset_already_included($file_location)) return;
?><link rel="stylesheet" href="<?= url_root().$file_location ?>?v=<?= filemtime($file_location) ?>" /><?php
}
# **************************** GENERAL UTILITY FUNCTIONS ******************************
// escapes a string for use in HTML attributes
function asafe($s)
{
if($s === null) return '';
return htmlspecialchars(str_replace(array("\r", "\n"), ' ', $s), ENT_QUOTES, 'UTF-8');
}
// escapes a string for use in HTML text
function safe($s)
{
if($s === null) return '';
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function jsafe($s)
{
return json_encode($s);
}
function clamp($v, $min, $max)
{
if($v < $min) return $min;
if($v > $max) return $max;
return $v;
}
function pick_entry_from_range($array, $value)
{
if(!is_array($array)) return [];
$result = [];
foreach($array as $pv)
if($value >= $pv['from'] && $value <= $pv['to']) $result = $pv;
return $result;
}
/**
* Can have any number of arguments. Returns the first of its arguments that is not false, empty string, or null.
*/
function first()
{
$args = func_get_args();
foreach($args as $v)
{
if(isset($v) && $v !== false && $v !== '' && $v !== null)
return($v);
}
return('');
}
function alnum($s, $replace_with = '_', $trim = true)
{
if($trim) $s = trim(strtolower($s));
return(preg_replace("/[^[:alnum:][:space:]]/u", $replace_with, $s));
}
/**
* Append a string to the given file.
*/
function write_to_file($filename, $content)
{
if (is_array($content)) $content = json_encode($content);
$open = fopen($filename, 'a+');
fwrite($open, $content);
fclose($open);
@chmod($filename, 0777);
}
# **************************** ARRAY FUNCTIONS ******************************
/**
* Returns a value from the $GLOBALS['config'] array identified by $key.
* Sub-array values can be addressed by using the '/' character as a separator.
*/
function cfg($key)
{
$config = $GLOBALS['config'];
$seg = explode('/', $key);
$lastSeg = array_pop($seg);
foreach($seg as $s)
{
if(is_array($config) && array_key_exists($s, $config) && is_array($config[$s]))
$config = $config[$s];
else
return null;
}
if(!is_array($config) || !array_key_exists($lastSeg, $config))
return null;
return($config[$lastSeg]);
}
# **************************** STRING/FORMATTING FUNCTIONS ******************************
/**
* Convert any base number into another number of another base system.
*/
function base_convert_any($numberInput, $fromBaseInput, $toBaseInput)
{
if ($fromBaseInput==$toBaseInput) return $numberInput;
$fromBase = str_split($fromBaseInput,1);
$toBase = str_split($toBaseInput,1);
$number = str_split($numberInput,1);
$fromLen=strlen($fromBaseInput);
$toLen=strlen($toBaseInput);
$numberLen=strlen($numberInput);
$retval='';
if ($toBaseInput == '0123456789')
{
$retval=0;
for ($i = 1;$i <= $numberLen; $i++)
$retval = bcadd($retval, bcmul(array_search($number[$i-1], $fromBase),bcpow($fromLen,$numberLen-$i)));
return $retval;
}
if ($fromBaseInput != '0123456789')
$base10=base_convert_any($numberInput, $fromBaseInput, '0123456789');
else
$base10 = $numberInput;
if ($base10<strlen($toBaseInput))
return $toBase[$base10];
while($base10 != '0')
{
$retval = $toBase[bcmod($base10,$toLen)].$retval;
$base10 = bcdiv($base10,$toLen,0);
}
return $retval;
}
/**
* Convert a Unix timestamp into a human-friendly short form.
*/
function age_to_string($unixDate, $new = 'just now', $ago = 'ago')
{
if($unixDate == 0) return('-');
$result = '';
$oneMinute = 60;
$oneHour = $oneMinute*60;
$oneDay = $oneHour*24;
$difference = time() - $unixDate;
if ($difference < $oneMinute)
$result = $new;
else if ($difference < $oneHour)
$result = round($difference/$oneMinute).' min '.$ago;
else if ($difference < $oneDay)
$result = floor($difference/$oneHour).' h '.$ago;
else if ($difference < $oneDay*5)
$result = gmdate('D H:i', $unixDate);
else if ($difference < $oneDay*365)
$result = gmdate('M dS H:i', $unixDate);
else
$result = date('d. M Y H:i', $unixDate);
return($result);
}
/**
* Given the separator string $segdiv, cut a piece of &$cake off that precedes $segdiv,
* and return that piece. If there are no instances of $segdiv in &$cake, nibble()
* returns the entirety of &$cake and sets &$cake to an empty string.
*/
function nibble($segdiv, &$cake, &$found = false)
{
$p = strpos($cake, $segdiv);
if ($p === false)
{
$result = $cake;
$cake = '';
$found = false;
}
else
{
$result = substr($cake, 0, $p);
$cake = substr($cake, $p + strlen($segdiv));
$found = true;
}
return $result;
}
function make_hash($s = false, $length = 10)
{
if($s === false) $s = time().random_int(0, 2147483647);
$s = strtolower(substr(trim($s), 0, 64));
return(substr(base_convert_ex(
sha1(sha1('qw0e983124o521öl34u9087'.$s)),
'0123456789abcdef',
'0123456789abcdefghijklmnopqrstuvwxyz'
), -$length));
}
+126
View File
@@ -0,0 +1,126 @@
<?php
class URL
{
static $locator = '';
static $route = array();
static $error = '';
static $title = 'TITLE';
static $page_type = 'html';
static $fragments = [];
# extracts the locator string from parameters or the URI
static function ParseRequestURI()
{
$uri = (string)first($_SERVER['REQUEST_URI']);
$parsed_uri = parse_url($uri);
$loc = (string)($parsed_uri['path'] ?? '');
$query_string = (string)($parsed_uri['query'] ?? '');
if($query_string !== '')
{
$route_found = false;
$query_parts = explode('&', $query_string);
$named_parts = array();
foreach($query_parts as $query_part)
{
if($query_part === '')
continue;
if(!$route_found && str_contains($query_part, '=') === false)
{
$loc = rawurldecode($query_part);
$route_found = true;
continue;
}
$named_parts[] = $query_part;
}
if(sizeof($named_parts) > 0)
{
$params = array();
parse_str(implode('&', $named_parts), $params);
$_REQUEST = array_merge($_REQUEST, $params);
}
}
self::$locator = $loc;
return($loc);
}
static function NotFound($message = 'resource not found')
{
header("HTTP/1.0 404 Not Found");
self::$error = $message;
}
static $tried = array();
# determines which view to show given a locator string
static function MakeRoute($lc = false)
{
$route = $GLOBALS['config']['url'];
if(!$lc)
$lc = URL::ParseRequestURI();
if(str_starts_with($lc, $GLOBALS['config']['url']['root']))
$lc = substr($lc, strlen($GLOBALS['config']['url']['root']));
$seg = array();
foreach(explode('/', $lc) as $s)
if(substr($s, 0, 1) != '.' && $s != '') # strip unnecessary prefixes
$seg[] = $s;
$route['l-path'] = implode('/', $seg);
$route['page'] = first($seg[0] ?? false, 'index');
if(isset($seg[0]) && $seg[0] !== '' && substr($seg[0], 0, 1) == ':')
{
$route['page'] = substr($route['l-path'], 1);
}
if(sizeof(self::$route) == 0) self::$route = $route;
return($route);
}
static function ResolveViewFile($base_dir = 'views')
{
$lpath = first(self::$route['l-path'], 'index');
$base_dir = rtrim($base_dir, '/');
$content_file = $base_dir.'/'.$lpath;
if(file_exists($content_file.'.php'))
return array('file' => $content_file.'.php');
$dir_index_file = $content_file.'/index.php';
if(file_exists($dir_index_file))
return array('file' => $dir_index_file);
$lpath_parts = array_values(array_filter(explode('/', $lpath), function($segment) {
return $segment !== '';
}));
if(sizeof($lpath_parts) > 1)
{
$last_seg = array_pop($lpath_parts);
$parent_file = $base_dir.'/'.implode('/', $lpath_parts).'/index.php';
if(file_exists($parent_file))
return array('file' => $parent_file, 'param' => $last_seg);
}
return false;
}
static function Link($path, $params = false)
{
$path = ltrim((string)$path, '?');
$query = $params !== false ? http_build_query($params) : '';
if(cfg('url/pretty'))
{
return($GLOBALS['config']['url']['root'].$path.($query !== '' ? '?'.$query : ''));
}
else
{
if($path === '')
return($GLOBALS['config']['url']['root'].($query !== '' ? '?'.$query : ''));
return($GLOBALS['config']['url']['root'].'?'.$path.($query !== '' ? '&'.$query : ''));
}
}
# redirect to URL and quit
static function Redirect($url = '', $params = array())
{
header('location: '.self::Link($url, $params));
die();
}
}
+134
View File
@@ -0,0 +1,134 @@
# url.class.php
A PHP URL routing and handling class for web applications with support for pretty URLs and request parsing.
## Static Properties
```php
URL::$locator // Current URL path
URL::$route // Parsed route information
URL::$error // Error message for 404s
URL::$title // Page title
URL::$page_type // Response type (default: 'html')
URL::$fragments // URL fragments array
URL::$tried // Attempted routes (debugging)
```
## Request Parsing
```php
URL::ParseRequestURI() // Extract locator from REQUEST_URI
```
Parses URLs and populates `$_REQUEST` with query parameters. Handles both pretty URLs and query string format.
## Route Generation
```php
URL::MakeRoute($locator) // Generate route from URL path
```
Creates route array with:
- `l-path` - Clean URL path segments
- `page` - Target page/view (defaults to 'index')
- URL config values
### Special Route Handling
- Paths starting with `:` use the entire path as page name
- Strips dots and empty segments for security
- Removes root path prefix
## URL Generation
```php
URL::Link($path, $params) // Generate application URL
```
Creates URLs based on `cfg('url/pretty')` setting:
- **Pretty URLs**: `/path?param=value`
- **Query URLs**: `/?path&param=value`
## Navigation
```php
URL::Redirect($url, $params) // Redirect and exit
URL::NotFound($message) // Send 404 response
```
## Examples
### Basic Routing
```php
// Parse current request
$locator = URL::ParseRequestURI(); // Returns 'users/profile'
$route = URL::MakeRoute(); // Creates route array
// Access route data
echo URL::$route['l-path']; // 'users/profile'
echo URL::$route['page']; // 'index' (default)
```
### URL Generation
```php
// Generate links
$userLink = URL::Link('users/123'); // '/users/123'
$searchLink = URL::Link('search', ['q' => 'term']); // '/search?q=term'
// Redirect examples
URL::Redirect('login'); // Redirect to login
URL::Redirect('dashboard', ['tab' => 'settings']); // With parameters
```
### Special Page Routes
```php
// URL: /special/admin/users
// If URL starts with ':special', entire path becomes page name
URL::MakeRoute(':special/admin/users');
// Results in: page = 'special/admin/users'
```
### Error Handling
```php
// Set 404 status
URL::NotFound('Page not found');
echo URL::$error; // 'Page not found'
// Check attempted routes
var_dump(URL::$tried); // Array of attempted route resolutions
```
## Configuration Integration
Uses configuration values:
- `cfg('url/root')` - Application root path
- `cfg('url/pretty')` - Enable/disable pretty URLs
- `$GLOBALS['config']['url']` - URL configuration array
## URL Parsing Logic
1. **Extract Path**: Removes query string from REQUEST_URI
2. **Parse Parameters**: Converts query string to $_REQUEST entries
3. **Clean Segments**: Removes dots, empty segments, security prefixes
4. **Route Resolution**: Determines target page and path
5. **Special Handling**: Processes colon-prefixed paths
## Security Features
- **Dot Prevention**: Strips segments starting with '.'
- **Path Sanitization**: Removes empty and problematic segments
- **Root Stripping**: Removes application root from paths
## Pretty URL Support
Supports both URL formats:
```php
// Pretty URLs (url/pretty = true)
/users/profile/edit
/search?q=term
// Query URLs (url/pretty = false)
/?users/profile/edit
/?search&q=term
```
+111
View File
@@ -0,0 +1,111 @@
<?php
class User
{
public static $session_key = 'user_id';
public static $current_profile = null;
static function LoadById($id)
{
if (empty($id)) return false;
$data = Filebase::read_data('users', Filebase::hash($id), 'account');
if (!$data) return false;
$data['id'] = $id;
self::$current_profile = $data;
return $data;
}
static function SaveById($id)
{
if (empty(self::$current_profile)) return false;
if (empty($id)) return false;
Filebase::write_data('users', Filebase::hash($id), 'account', self::$current_profile);
return true;
}
static function Create($basic_profile)
{
if (empty($basic_profile['email']) || empty($basic_profile['password'])) {
return ['result' => false, 'message' => 'email_and_password_required'];
}
$email = strtolower(trim($basic_profile['email']));
$existing = Filebase::read_data('users', Filebase::hash($email), 'account');
if ($existing) return ['result' => false, 'message' => 'user_exists'];
$now = time();
$stored = $basic_profile;
$stored['email'] = $email;
$stored['password_hash'] = password_hash($basic_profile['password'], PASSWORD_DEFAULT);
$stored['created'] = $now;
if (!isset($stored['roles'])) $stored['roles'] = ['user'];
Filebase::write_data('users', Filebase::hash($email), 'account', $stored);
return ['result' => true, 'id' => $email, 'profile' => $stored];
}
static function AuthWithPassword($password, $basic_profile = false)
{
if ($basic_profile === false) {
if (!isset($_POST['email'])) return ['result' => false, 'message' => 'email_missing'];
$email = strtolower(trim($_POST['email']));
$basic_profile = Filebase::read_data('users', Filebase::hash($email), 'account');
if ($basic_profile) $basic_profile['id'] = $email;
}
if (!$basic_profile) return ['result' => false, 'message' => 'no_such_user'];
if (!isset($basic_profile['password_hash'])) return ['result' => false, 'message' => 'no_password_set'];
if (password_verify($password, $basic_profile['password_hash'])) {
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
$_SESSION[self::$session_key] = $basic_profile['id'];
self::$current_profile = $basic_profile;
return ['result' => true, 'profile' => $basic_profile];
}
return ['result' => false, 'message' => 'invalid_password'];
}
static function Permission($thing_name)
{
if (!self::IsSignedIn()) return false;
$roles = self::$current_profile['roles'] ?? [];
if (in_array('admin', $roles)) return true;
$perm_map = [
'edit' => ['admin', 'editor'],
'view' => ['admin', 'editor', 'user'],
];
$allowed = $perm_map[$thing_name] ?? ['admin'];
return count(array_intersect($roles, $allowed)) > 0;
}
static function IsSignedIn()
{
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
if (isset($_SESSION[self::$session_key]) && $_SESSION[self::$session_key]) {
if (self::$current_profile) return true;
$id = $_SESSION[self::$session_key];
$profile = Filebase::read_data('users', Filebase::hash($id), 'account');
if ($profile) {
$profile['id'] = $id;
self::$current_profile = $profile;
return true;
}
unset($_SESSION[self::$session_key]);
}
return false;
}
static function Logout()
{
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
unset($_SESSION[self::$session_key]);
self::$current_profile = null;
return true;
}
}