trying to port web app starter from PHP
This commit is contained in:
+199
@@ -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
@@ -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();
|
||||
|
||||
Executable
+144
@@ -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
@@ -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
@@ -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(['<', '>'], '',
|
||||
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('"', '"', $p_attributes['text:description']), true);
|
||||
if($decoded !== null) {
|
||||
$p_prop = $decoded;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$p_prop['format'] = $p_attributes['text:description'];
|
||||
}
|
||||
}
|
||||
$p_content = str_replace(array('>', '<'), '',
|
||||
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
@@ -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
@@ -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
|
||||
}
|
||||
Executable
+113
@@ -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
|
||||
Executable
+254
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Executable
+134
@@ -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¶m=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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user