merge templates/ and classes/ from 3.x

This commit is contained in:
Gudule Lapointe
2025-03-15 13:41:42 +01:00
parent dec05dfb65
commit 5563ce3b1e
14 changed files with 2025 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* OpenSim_Exception class
*
* This class extends the Exception class to force logging of all exceptions.
*
* @package magicoli/opensim-helpers
*/
class OpenSim_Exception extends Exception {
// Properties defined by parent class, for reference:
// protected string $message = "";
// private string $string = "";
// protected int $code;
// protected string $file = "";
// protected int $line;
// private array $trace = [];
// private ?Throwable $previous = null;
public function __construct( $message, $code = 0, Exception $previous = null ) {
parent::__construct( $message, $code, $previous );
error_log( $this->__toString() );
}
// Disabled custom string representation of the exception, it is worst than the default one.
// public function __toString() {
// $prefix = '';
// // return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
// $message = strip_tags( $this->message );
// $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
// if( ! empty( $trace[1] ) ) {
// $class = $trace[1]['class'] ?? '';
// $function = $trace[1]['function'] ?? '';
// }
// if( ! empty( trim ( $class . $function ) ) ) {
// $prefix .= '(' . ( empty( $class ) ? '' : $class . '::' ) . $function . ') ';
// }
// return $prefix . $message;
// }
}
/**
* This is temporary, it's misleading to replace Errors with Exceptions,but it's a way
* to make sure I can replace all new Error() calls with new OpenSim_Exception() calls.
*
* I pledge to check soon, but I spend too much time finetuning the Error catchers, so I
* want to keep a way to switch back fast if needed.
*
* TODO:
* - Test again every use case where OpenSim_Error is used, make sure interrupts happen as expected.
* - Replace all OpenSim_Error calls with OpenSim_Exception.
* - Remove this class.
*/
class OpenSim_Error extends OpenSim_Exception {
public function __construct( $message, $code = 0, Exception $previous = null ) {
parent::__construct( $message, $code, $previous );
// error_log( $this->__toString() );
}
}
+486
View File
@@ -0,0 +1,486 @@
<?php
/**
* TODO
*
* Form class for OpenSimulator Helpers
*
* Handles form rendering and global processing. Forms definition are passed by calling class.
*
* Methods:
* - register( $args, $fields ) register a new form (used in _construct)
* $args = array(
* 'id' => unique id
* 'html' => html code
* 'callback' => callback to call to process form
* )
* - render_form() return the form html code
* - get_values() return an array of field_id => value pairs
* - get_fields() return the array of defined fields
* - process() process the form callback
*
* @package magicoli/opensim-helpers
**/
require_once( dirname(__DIR__) . '/classes/init.php' );
class OpenSim_Form {
private $form_id;
private $fields = array();
private $callback;
private static $forms = array();
private $errors;
private $html;
private $completed;
private $multistep;
public $tasks;
public function __construct($args = array(), $step = 0) {
if( is_string( $args )) {
// If only a string is passed, consider it as form_id for a pending form
$args = array( 'form_id' => $args );
}
if (!is_array($args)) {
error_log(__METHOD__ . ' invalid argument type ' . gettype($args));
throw new InvalidArgumentException('Invalid argument type: ' . gettype($args));
}
$args = OpenSim::parse_args($args, array(
'form_id' => uniqid('form-', true),
'fields' => array(),
'callback' => null,
'multistep' => false,
));
$this->form_id = $args['form_id'];
$this->multistep = $args['multistep'];
$this->steps = $args['steps'] ?? false;
$this->callback = $args['callback'];
$this->add_fields($args['fields']);
$this->completed = $_SESSION[$this->form_id]['completed'] ?? $this->completed;
self::$forms[$this->form_id] = $this;
// $this->refresh_steps();
}
/**
* Static factory method to register an instance of OpenSim_Form.
* Handles exceptions internally to avoid requiring try-catch blocks during instantiation.
*
* @param array $args Arguments for form initialization.
* @param int $step Optional step parameter.
* @return OpenSim_Form|false Returns an instance of OpenSim_Form on success, or false on failure.
*/
public static function register($args = array(), $step = 0) {
try {
return new self($args, $step);
} catch (InvalidArgumentException $e) {
error_log($e->getMessage());
OpenSim::notify_error($e->getMessage() );
return false;
}
}
public function add_steps($steps) {
if( empty( $steps )) {
return false;
}
$this->steps = OpenSim::parse_args( $steps, $this->steps );
}
public function add_fields( $fields) {
if( empty( $fields )) {
return;
}
$this->fields = OpenSim::parse_args( $fields, $this->fields );
$this->get_next_step();
}
public function task_error( $field_id, $message, $type = 'warning' ) {
$this->errors[$field_id] = array(
'message' => $message ?? 'Error',
'type' => empty( $type ) ? 'warning' : $type,
);
}
public function render_form() {
if( ! empty( $this->html )) {
return $this->html;
}
if( $this->multistep ) {
$this->refresh_steps();
$fields = $this->get_step_fields() ?? array();
if( ! is_array( $fields )) {
error_log( __METHOD__ . ' wrong field format ' );
return false;
}
} else {
$fields = $this->fields;
}
$form_id = $this->form_id;
// Update Reset button to remain a submit button and bypass validation
$reset_button = ( empty($_SESSION[$form_id])) ? '' : sprintf(
'<button type="submit" name="reset" formnovalidate class="btn btn-secondary bg-black-50 mx-2">%s</button>',
_( 'Reset Form' )
);
if( empty( $fields ) && empty( $reset_button ) ) {
error_log( __METHOD__ . ' called with empty fields' );
return false;
}
$html = '';
$fields = empty($fields) ? array() : $fields;
foreach ( $fields as $field => $data ) {
$add_class = '';
$add_attrs = '';
if( ! empty( $this->errors[$field] ) ) {
$field_error = $this->errors[$field];
$data['help'] = OpenSim::error_html( $field_error, 'warning' ) . ( $data['help'] ?? '' );
if( $field_error['type'] == 'danger' ) {
$add_class .= ' is-invalid';
}
}
if( $data['type'] == 'plaintext' ) {
$data['type'] = 'text';
$add_class .= ' form-control-plaintext';
}
if( isset( $data['readonly'] ) && $data['readonly'] ) {
$add_class .= ' text-muted';
$add_attrs .= ' readonly';
}
$add_attrs .= isset( $data['disabled'] ) && $data['disabled'] ? ' disabled' : '';
$add_attrs .= isset( $data['required'] ) && $data['required'] ? ' required' : '';
// $placeholder = isset( $data['placeholder'] ) ? $data['placeholder'] : '';
$html .= sprintf(
'<div class="form-group py-1">
<label for="%s">%s</label>
<input type="%s" name="%s" class="form-control %s" value="%s" placeholder="%s" %s>
<small class="form-text text-muted">%s</small>
</div>',
$field,
$data['label'],
$data['type'],
$field,
$add_attrs . $add_class,
$_POST[$field] ?? $data['value'] ?? $data['default'] ?? '',
$data['placeholder'] ?? '',
$add_attrs,
$data['help'] ?? ''
);
}
$submit = empty( $html ) ? '' : sprintf(
'<button type="submit" class="btn btn-primary">%s</button>',
_('Submit')
);
$buttons = sprintf(
'<input type="hidden" name="form_id" value="%s">'
. '<input type="hidden" name="step_key" value="%s">'
. '<div class="form-group text-end">%s</div>',
$this->form_id,
$this->next_step_key ?? '',
$reset_button . $submit
);
$html = sprintf(
'<form id="%s" method="post" action="%s" class="py-4">%s</form>',
$this->form_id,
$_SERVER['PHP_SELF'],
$html . $buttons
);
OpenSim::enqueue_script( 'form', 'js/form.js' );
OpenSim::enqueue_style( 'form', 'css/form.css' );
$this->html = $html;
return $html;
}
public function process() {
if( empty( $_POST )) {
// Only init values if form is not submitted
return $this->get_values();
}
if (is_callable($this->callback)) {
return call_user_func($this->callback, $this->get_values());
} else {
if (is_array($this->callback)) {
$callback_name = get_class($this->callback[0]) . '::' . $this->callback[1];
} else {
$callback_name = $this->callback;
}
error_log( $callback_name . ' is not callable from ' . __METHOD__ );
return false;
}
}
/**
* Get values from fields definition and post.
*
* TODO: make sure values are not replaced with post values before this step,
* although it doesn't hurt with the current usage, it might be useful to
* compare old and new value in the process() method called later.
*/
public function get_values() {
$form_id = $this->form_id;
$values = array();
if( $this->multistep ) {
$fields = $this->get_step_fields() ?? array();
if( ! is_array( $fields )) {
error_log( __METHOD__ . ' wrong field format ' );
return false;
}
} else {
$fields = $this->fields;
}
// error_log( 'Fields: ' . print_r( $fields, true ) );
foreach( $fields as $key => $field ) {
$values[$key] = $_POST[$key] ?? $_SESSION[$form_id][$key] ?? $field['value'] ?? null;
}
return $values;
}
private function get_step_fields() {
if( ! isset( $this->next_step_key ) || ! isset( $this->fields[$this->next_step_key] ) ) {
return array();
}
return $this->fields[$this->next_step_key];
}
// Get defined fields
public function get_fields() {
return $this->fields;
}
public function get_form( $form_id ) {
if( empty( $form_id )) {
return false;
}
return isset( self::$forms[$form_id] ) ? self::$forms[$form_id] : false;
}
public function get_forms() {
return self::$forms ?? false;
}
public function get_next_step() {
if( empty( $this->steps )) {
return false;
}
$steps = $this->steps;
$current_step = array_search($this->completed, array_keys($steps));
if( empty( $this->completed ) ) {
$next_step_key = key($steps);
$next_step_label = $steps[$next_step_key];
} else {
$next_step_key = array_keys($steps)[$current_step + 1] ?? null;
if( empty($steps[$next_step_key])) {
$next_step_key='completed';
$next_step_label = _('Completed');
} else {
$next_step_label = $steps[$next_step_key] ?? null;
}
}
// $this->next_step = $next_step_label;
$this->next_step_key = $next_step_key;
$this->tasks = $this->steps[$next_step_key]['tasks'] ?? false;
return $this->next_step_key;
}
/**
* Use the value of $this->complete as last completed step, get the next step and
* build a navigation html.
*/
private function refresh_steps() {
if( empty( $this->steps )) {
return false;
}
$steps = $this->steps;
if( ! empty($_POST['form_id']) ) {
$form_id = $_POST['form_id'];
$form = self::$forms[$form_id];
if( $form ) {
$form->process();
} else {
error_log( __METHOD__ . ' Form ' . $form_id . ' is not registered' );
return false;
}
}
$next_step_key = $this->get_next_step();
// Set progression table
$progress = array();
$status = 'completed';
foreach( $steps as $key => $step ) {
if( $key == $next_step_key ) {
$progress[$key] = 'active';
$status = '';
} else {
$progress[$key] = $status;
}
}
$this->progression = $progress;
}
/**
* Build HTML progress bar with bootstrap classes
*/
public function render_progress() {
$this->refresh_steps();
if( empty( $this->steps )) {
return false;
}
$steps = $this->steps;
$progress = $this->progression;
$status = 'completed';
$html = '<ul class="nav nav-tabs nav-fill">';
foreach( $steps as $key => $step ) {
$status = $progress[$key] ?? 'disabled';
$label = $steps[$key]['label'] ?? $key;
$style = '';
switch( $status ) {
case 'completed':
$status .= ' text-success';
$label .= ' &#10003;';
// $style = 'style="color:green"';
break;
case 'active':
$status = 'active bg-secondary';
$style = 'style="font-weight:bold"';
break;
}
$status = empty($status) ? 'disabled' : $status;
// if( $key == $next_step_key ) {
// $progress[$key] = 'active';
// $status = '';
// } else {
// $progress[$key] = $status;
// }
// '<div class="progress-bar progress-bar-striped progress-bar-animated bg-%s" role="progressbar" style="width: 20%%" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100">%s</div>',
$html .= sprintf( '<li class="nav-item">
<a class="nav-link %s" aria-current="page" href="#" %s>%s</a>
</li>',
$status,
$style,
$label,
);
}
$html .= '</ul>';
return $html;
}
public function validate_file( $field_id, $file_path, $strict = false ) {
if( empty( $file_path ) ) {
if( $strict ) {
$message = sprintf( _('File %s is required'), $field_id );
$this->task_error( $field_id, $message, 'danger' );
return false;
}
// if not strict, allow it to be empty
return true;
}
if( ! file_exists( $file_path )) {
$message = sprintf( _('File %s not found'), $file_path );
$this->task_error( $field_id, $message, 'danger' );
return false;
}
return true;
}
/**
* Make sure the file is a valid .ini file
*/
public function is_valid_ini_file( $field_id, $file_path, $strict = false ) {
if( ! $this->validate_file( $field_id, $file_path, true )) {
$this->task_error( $field_id, _('File not found'), 'danger' );
return false;
}
// If strict, use parse_ini_file
if( $strict ) {
$ini = parse_ini_file( $file_path );
if( empty( $ini )) {
$message = sprintf( _('File %s does not comply with .ini standards.'), $file_path );
$this->task_error( $field_id, $message, 'danger' );
// throw new Exception( $message );
return false;
}
return true;
}
// If not strict, a light check is enough
//
// OpenSim uses some non-standard formatting that are not supported by parse_ini_file.
// Ignore comments and empty lines, then make sure the remaining contains
// only key = value pairs or [sections]
$ini = file_get_contents( $file_path );
$lines = explode( "\n", $ini );
$valid = true;
// Filter out comments and empty lines
$lines = array_filter( array_map( 'trim', $lines ), function( $line ) {
return ! empty( $line ) && ! preg_match( '/^\s*;/', $line );
});
// Filter out valid lines, leaving only invalid ones
$valid = array_filter( array_map( function( $line ) {
return preg_match( '/^\[.*\]$|.*=.*$/', $line ) ? false : $line;
}, $lines ));
if( ! empty( $valid )) {
$message = sprintf( _('File %s is not a valid .ini file'), $file_path );
error_log( $message . ', found invalid lines: ' . print_r( $valid, true ) );
$this->task_error( $field_id, $message, 'danger' );
return false;
}
return true;
}
/**
* Make sure the file is a valid Robust.ini file
*/
public function is_robust_ini_file( $field_id, $file_path ) {
if( ! $this->is_valid_ini_file( $field_id, $file_path )) {
throw new Exception( _('Not a valid ini file') );
// return false;
}
$required_sections = array(
'DatabaseService',
'GridInfoService',
'LoginService',
);
// Check if all required sections are present, with array_map (without parse_ini_file)
$ini = file_get_contents( $file_path );
$lines = explode( "\n", $ini );
$sections = array_map( function( $line ) {
if( preg_match( '/^\[(.*)\]\s*$/', $line, $matches )) {
return $matches[1];
}
return false;
}, $lines );
$missing = array_diff( $required_sections, $sections );
if( ! empty( $missing )) {
$message = sprintf( _('Not a valid Robust config file.'), $file_path, implode( ', ', $missing ));
$this->task_error( $field_id, $message, 'danger' );
$message = sprintf( _('%s is missing required sections: %s'), $file_path, '<ul><li>' . implode( '</li><li>', $missing ) . '</li></ul>' );
throw new Exception( $message );
}
return true;
}
public function complete( $step ) {
$this->completed = $step;
$_SESSION[$this->form_id]['completed'] = $step;
$this->refresh_steps();
}
}
+290
View File
@@ -0,0 +1,290 @@
<?php
/**
* Grid class for OpenSimulator Helpers
*/
if( ! defined( 'OSHELPERS' ) ) {
exit;
}
class OpenSim_Grid {
private static $grid_stats;
private $grid_info;
private $grid_info_card;
private $grid_stats_card;
private static $labels = array();
public function __construct() {
$this->constants();
// $this->grid_stats = $this->get_grid_stats();
// $this->grid_info = $this->get_grid_info();
// $this->grid_info_card = $this->get_grid_info_card();
// $this->grid_stats_card = $this->get_grid_stats_card();
}
public function constants() {
self::$labels = array(
'status' => _('Status'),
'members' => _('Members'),
'active_members' => _('Active members (30 days)'),
'members_in_world' => _('Members in world'),
'active_users' => _('Active users (30 days)'),
'total_users' => _('Total users in world'),
'regions' => _('Regions'),
'total_area' => _('Total area'),
);
}
public static function get_grid_info( $grid_uri = false, $args = array() ) {
$info = array();
$HomeURI = OpenSim::get_option( 'Hypergrid.HomeURI' );
if( ! $grid_uri || $grid_uri === $HomeURI ) {
$is_local_grid = true;
// Default, get login_uri from config, query grid for live grid_info
$grid_uri = OpenSim::get_option( 'Hypergrid.HomeURI' );
if( empty( $grid_uri ) ) {
return false;
}
} else {
$is_local_grid = false;
// External grid lookup, not yet implemented
$info = array(
'Grid Name' => 'External Grid, not implemented.',
);
return false;
}
// Fetch live info from grid using $login_uri/get_grid_info and parse xml result in array
// Example xml result:
$xml_url = $grid_uri . '/get_grid_info';
try {
$xml = simplexml_load_file( $xml_url );
} catch( Exception $e ) {
$xml = false;
}
if( ! $xml ) {
$info = array(
'online' => false,
'login' => $grid_uri,
);
} else {
$info['online'] = true;
try {
$array = (array) $xml;
if( ! $array ) {
throw new Exception( 'Error parsing grid info.' );
}
$info = array_merge( $info, $array );
} catch( Exception $e ) {
return $e;
}
}
if( $is_local_grid ) {
$config_info = OpenSim::get_option( 'GridInfoService' );
if( is_array( $config_info ) ) {
$info = array_merge( $config_info, $info );
}
}
return $info;
}
public static function array_to_card( $id, $info, $args = array() ) {
if( empty( $info ) ) {
return;
}
$hide_first = '';
if( ! empty( $args['title'] ) && is_string( $args['title'] ) ) {
$title = $args['title'];
} else {
$title = array_values( $info )[0];
if( is_numeric( array_keys( $info )[0] ) || ( isset($args['hide_first']) && $args['hide_first'] === true ) ) {
$hide_first = 'hidden d-none';
}
}
$collapse_head = '';
$collapse_class = '';
$collapse_data = '';
$html = sprintf(
'<div id="card-%1$s" class="accordion flex-fill">
<div class="accordion-item card card-$1$s bg-primary">
<h5 class="card-title p-0 m-0 accordion-header" %4$s>
<button class="accordion-button p-3" data-bs-toggle="collapse" href="#card-list-%1$s" aria-expanded="true" aria-controls="collapse-card-list-%1$s">
%2$s
</button>
</h5>
<ul id="card-list-%s" class="list-group list-group-flush accordion-collapse collapse show" data-bs-parent="#card-%1$s">',
$id,
$title,
$collapse_head,
$collapse_class,
$collapse_data
);
foreach( $info as $key => $value ) {
$html .= sprintf(
'
<li class="list-group-item %2$s">
%3$s %4$s
</li>',
$id,
$hide_first,
is_numeric( $key ) ? '' : $key . ':',
$value
);
$hide_first = '';
$class="";
}
$html .= '</ul>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* Get grid information as a card
*/
public static function grid_info_card( $grid_uri = false, $args = array() ) {
$grid_info = self::get_grid_info( $grid_uri, $args );
if( ! $grid_info || OpenSim::is_error( $grid_info ) ) {
return false;
}
$info = array(
_('Grid Name') => $grid_info['gridname'],
_('Login URI') => OpenSim::hop( $grid_info['login'] ),
);
$title = false;
if( ! empty( $args['title'])) {
$title = $args['title'] === true ? _( 'Grid Information' ) : $args['title'];
} else {
$title = _( 'Grid Information' );
}
return self::array_to_card( 'grid-info', $info, array(
'title' => $title,
) );
}
public static function grid_stats_card( $args = null ) {
$grid_stats = self::get_grid_stats( $args );
if( ! $grid_stats || OpenSim::is_error( $grid_stats ) ) {
return false;
}
$title = false;
if( ! empty( $args['title'])) {
$title = $args['title'] === true ? _( 'Grid Information' ) : $args['title'];
} else {
$title = _( 'Grid Status' );
}
return self::array_to_card( 'grid-status', $grid_stats, array(
'title' => $title,
) );
}
private static function array_to_xml($array, $xml) {
foreach($array as $key => $value) {
if(is_array($value)) {
$subnode = $xml->addChild($key);
self::array_to_xml($value, $subnode);
} else {
$xml->addChild($key, htmlspecialchars($value));
}
}
}
public static function get_grid_stats( $args = null ) {
$grid_info = self::get_grid_info();
if( empty( $grid_info ) ) {
return false;
}
$grid_uri = $grid_info['login'];
$args = array_merge(array(
'output' => 'array',
'title' => true,
));
$stats = array(
'status' => $grid_info['online'] ? _('Online') : _('Offline'),
);
$robust_db = OpenSim::$robust_db;
if ( ! $robust_db || OpenSim::is_error($robust_db) ) {
$stats['error'] = _('Database not connected.');
} else {
$lastmonth = time() - 30 * 86400;
$gridonline = $grid_info['online'] ? _('Yes') : _('No');
$filter = '';
// if ( get_option( 'w4os_exclude_models' ) ) {
// $filter .= "u.FirstName != '" . get_option( 'w4os_model_firstname' ) . "'
// AND u.LastName != '" . get_option( 'w4os_model_lastname' ) . "'";
// }
// if ( get_option( 'w4os_exclude_nomail' ) ) {
// $filter .= " AND u.Email != ''";
// }
if ( ! empty( $filter ) ) {
$filter = "$filter AND ";
}
$stats = array(
'status' => $grid_info['online'] ? _('Online') : _('Offline'),
'members' => $robust_db->get_var( "SELECT COUNT(*)
FROM UserAccounts as u WHERE $filter active=1"
),
'active_members' => $robust_db->get_var( "SELECT COUNT(*)
FROM GridUser as g, UserAccounts as u
WHERE $filter PrincipalID = UserID AND g.Login > :lastmonth",
array(
'lastmonth' => $lastmonth,
)
),
'members_in_world' => $robust_db->get_var( "SELECT COUNT(*)
FROM Presence AS p, UserAccounts AS u
WHERE $filter RegionID != '00000000-0000-0000-0000-000000000000'
AND p.UserID = u.PrincipalID;"
),
'active_users' => $robust_db->get_var( "SELECT COUNT(*)
FROM GridUser WHERE Login > :lastmonth",
array(
'lastmonth' => $lastmonth,
)
),
'total_users' => $robust_db->get_var( "SELECT COUNT(*)
FROM Presence WHERE RegionID != '00000000-0000-0000-0000-000000000000';"
),
'regions' => $robust_db->get_var( "SELECT COUNT(*)
FROM regions"
),
'total_area' => $robust_db->get_var( "SELECT round(sum(sizex * sizey / 1000000),2)
FROM regions"
) . '&nbsp;km²',
);
// Replace keys with values of self::$labels
$labels = self::$labels;
$labels = array_intersect_key( self::$labels, $stats );
$stats = array_combine( $labels, $stats );
}
switch( $args['output'] ) {
case 'xml':
$xml = new SimpleXMLElement('<gridstatus/>');
self::array_to_xml($stats, $xml);
return $xml->asXML();
case 'array':
return $stats;
default:
return $stats;
}
return $stats;
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* Ini class for OpenSimulator Helpers
*
* Handles the .ini format, including parsing and converting to arrays.
**/
class OpenSim_Ini {
private $file;
private $ini;
private $config = array();
private $raw_ini_array;
private static $user_notices;
public function __construct( $args ) {
if( empty( $args ) ) {
throw new OpenSim_Error( __FUNCTION__ .'() empty value received');
}
if( is_string( $args ) && file_exists( $args ) ) {
try {
$file_content = file_get_contents( $args );
} catch (Throwable $e) {
$this->notify_error( $e, 'Error reading file' );
}
$content = file_get_contents( $args );
$this->raw_ini_array = explode( "\n", $content );
} elseif( is_string( $args ) ) {
$this->raw_ini_array = explode( "\n", $args );
} elseif( is_array( $args ) ) {
$this->raw_ini_array = $args;
} else {
throw new OpenSim_Error( __CLASS__ .' accepts only string, array or file path value' );
}
$this->sanitize_and_parse( $this->raw_ini_array );
}
public function get_config() {
return $this->config;
}
public function get_ini() {
return $this->ini;
}
/**
* Sanitize an INI string. Make sure each value is encosed in quotes.
* Convert constants to their value.
*/
private function sanitize_and_parse() {
$this->ini = '';
$this->config = array();
$lines = $this->raw_ini_array;
$section = '_';
foreach ( $lines as $line ) {
$line = trim( $line );
if ( empty( $line ) || preg_match('/^\s*;/', $line ) ) {
$this->ini .= "$line\n";
continue;
}
$parts = explode( '=', $line );
if( preg_match( '/^\[[a-zA-Z]+\]$/' , $line)) {
$section = trim( $line, '[]' );
$this->ini .= "$line\n";
continue;
}
if ( count( $parts ) < 2 ) {
$this->ini .= "$line\n";
continue;
}
// use first part as key, the rest as value
$key = trim( array_shift( $parts ) );
$value = trim( implode( '=', $parts ), '\" ');
// Replace constants with their value for $config array, leave untouched for $ini.
$config_value = $value;
while ( preg_match( '/\${Const\|([a-zA-Z]+)}/', $config_value, $matches ) ) {
$const = $matches[1];
$config_value = str_replace( '${Const|' . $const . '}', $this->config['Const'][$const], $config_value );
}
$this->config[$section][$key] = $config_value;
if( is_numeric( $value ) || in_array( $value, array( "true", "false" ) ) ) {
$this->ini .= "$key = $value\n";
} else {
$this->ini .= "$key = \"$value\"\n";
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
class OpenSim_Locale {
const HTTP_ACCEPT_LANGUAGE_HEADER_KEY = 'HTTP_ACCEPT_LANGUAGE';
private static $detected = array();
// private static $lang;
// private static $locale;
public static function detect() {
if( !empty( self::$detected ) ) {
return self::$detected;
}
$httpAcceptLanguageHeader = static::getHttpAcceptLanguageHeader();
if ($httpAcceptLanguageHeader == null) {
return [];
}
$locales = static::getWeightedLocales($httpAcceptLanguageHeader);
$sortedLocales = static::sortLocalesByWeight($locales);
self::$detected = array_map(function ($weightedLocale) {
return $weightedLocale['locale'];
}, $sortedLocales);
if( empty( self::$detected ) ) {
self::$detected = ['en_US', 'en'];
}
return self::$detected;
}
public static function locale() {
$detected = self::detect();
return $detected[0] ?? 'en_US';
}
public static function lang() {
$detected = self::detect();
return $detected[1] ?? substr( self::locale(), 0, 2 ) ?? 'en';
}
private static function getHttpAcceptLanguageHeader() {
if (isset($_SERVER[static::HTTP_ACCEPT_LANGUAGE_HEADER_KEY])) {
return trim($_SERVER['HTTP_ACCEPT_LANGUAGE']);
} else {
return null;
}
}
private static function getWeightedLocales($httpAcceptLanguageHeader) {
if (strlen($httpAcceptLanguageHeader) == 0) {
return [];
}
$weightedLocales = [];
// We break up the string 'en-CA,ar-EG;q=0.5' along the commas,
// and iterate over the resulting array of individual locales. Once
// we're done, $weightedLocales should look like
// [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]]
foreach (explode(',', $httpAcceptLanguageHeader) as $locale) {
// separate the locale key ("ar-EG") from its weight ("q=0.5")
$localeParts = explode(';', $locale);
$weightedLocale = ['locale' => $localeParts[0]];
if (count($localeParts) == 2) {
// explicit weight e.g. 'q=0.5'
$weightParts = explode('=', $localeParts[1]);
// grab the '0.5' bit and parse it to a float
$weightedLocale['q'] = floatval($weightParts[1]);
} else {
// no weight given in string, ie. implicit weight of 'q=1.0'
$weightedLocale['q'] = 1.0;
}
$weightedLocales[] = $weightedLocale;
}
return $weightedLocales;
}
/**
* Sort by high to low `q` value
*/
private static function sortLocalesByWeight($locales) {
usort($locales, function ($a, $b) {
// usort will cast float values that we return here into integers,
// which can mess up our sorting. So instead of subtracting the `q`,
// values and returning the difference, we compare the `q` values and
// explicitly return integer values.
if ($a['q'] == $b['q']) {
return 0;
}
if ($a['q'] > $b['q']) {
return -1;
}
return 1;
});
return $locales;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
class OpenSim_Page {
protected $page_title;
protected $content;
public function __construct() {
}
public function get_page_title() {
return $this->page_title;
}
public function get_content() {
return $this->content;
}
public function get_sidebar_left() {
return '';
}
public function get_sidebar( $id = 'right' ) {
if( empty( $id ) ) {
return '';
}
$html = '';
switch( $id ) {
case 'left':
// $html = OpenSim_Grid::grid_stats_card();
break;
case 'right':
$html .= OpenSim_Grid::grid_info_card();
$html .= OpenSim_Grid::grid_stats_card();
break;
default:
break;
}
$class="flex";
// if( ! empty( $html ) ) {
// $html = sprintf(
// '<div id="sidebar-%s" class="sidebar sidebar-%s flex-row">%s</div>',
// $id,
// $id,
// $html
// );
// }
return $html;
}
}
+680
View File
@@ -0,0 +1,680 @@
<?php
/**
* OpenSim class
*
* This class is responsible for defining constants and loading all classes needed by all scripts.
*
* Classes needed only by some scripts are handled by themselves.
*
* @package magicoli/opensim-helpers
*/
// Start session if not already started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
class OpenSim {
private static $tmp_dir;
private static $user_notices = array();
private static $version;
private static $version_slug;
private static $scripts;
private static $styles;
private static $is_dev;
public static $robust_db;
public function __construct() {
// Check if domain name starts with "dev." or usual wp debug constants are set
self::$is_dev = ( strpos( $_SERVER['HTTP_HOST'], 'dev.' ) === 0 ) || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG );
}
public function init() {
$this->constants();
$this->includes();
$this->grid = new OpenSim_Grid();
}
public function constants() {
if( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', dirname( __FILE__ ) . '/' );
}
define( 'OSHELPERS', true );
define( 'OSHELPERS_DIR', self::trailingslashit( dirname( __DIR__ ) ) );
define( 'OSHELPERS_URL', self::get_helpers_url() );
// ( isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http" ) . "://$_SERVER[HTTP_HOST]$_SERVER[SCRIPT_NAME]" );
}
public function includes() {
require_once( OSHELPERS_DIR . 'classes/class-exception.php' );
require_once( OSHELPERS_DIR . 'includes/databases.php' );
require_once( OSHELPERS_DIR . 'includes/functions.php' );
$this->db_connect();
require_once( OSHELPERS_DIR . 'classes/class-locale.php' );
require_once( OSHELPERS_DIR . 'classes/class-ini.php' );
require_once( OSHELPERS_DIR . 'classes/class-grid.php' );
}
public function db_connect() {
$DatabaseService = self::get_option( 'DatabaseService', false );
$connectionstring = self::get_option( 'DatabaseService.ConnectionString', false);
if( $connectionstring ) {
$creds = self::connectionstring_to_array( $connectionstring );
$dsn = sprintf(
'mysql:host=%s;dbname=%s',
$creds['host'] . ( empty( $creds['port'] ) ? '' : ':' . $creds['port'] ),
$creds['name'],
);
$db = new OSPDO( $dsn, $creds['user'], $creds['pass'] );
if( $db->connected ) {
$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$db->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
self::$robust_db = $db;
}
} else {
return false;
}
}
public function get_helpers_url() {
$helpers_path = dirname( __DIR__ );
$url_path = self::trailingslashit( str_replace( $_SERVER['DOCUMENT_ROOT'], '', $helpers_path ) );
$parsed = array(
'scheme' => isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http',
'host' => $_SERVER['HTTP_HOST'],
);
$url = self::build_url( $parsed ) . ltrim( $url_path );
return $url;
}
public static function get_version( $sanitized = false ) {
if( $sanitized && self::$version_slug ) {
return self::$version_slug;
} else if ( ! $sanitized && self::$version ) {
return self::$version;
}
if( file_exists( OSHELPERS_DIR . '.version' ) ) {
$version = file_get_contents( '.version' );
} else {
$version = '0.0.0';
}
if( file_exists( '.git/HEAD' ) ) {
$hash = trim( file_get_contents( '.git/HEAD' ) );
$hash = trim( preg_replace( '+.*[:/]+', '', $hash ) );
if( !empty( $hash ) && file_exists( '.git/refs/heads/' . $hash ) ) {
$hash = substr( file_get_contents( '.git/refs/heads/' . $hash ), 0, 7 ) . " ($hash)";
} else {
$hash = substr( $hash, 0, 7 );
$hash .= ' (detached)';
}
$version .= empty( $hash ) ? ' git ' : ' git ' . $hash;
self::$is_dev = ( empty( $hash ) ) ? self::$is_dev : true;
}
self::$version = $version;
self::$version_slug = self::sanitize_slug( $version );
if( $sanitized && self::$version_slug ) {
return self::$version_slug;
}
return $version;
}
public static function get_temp_dir( $dir = false ) {
if( isset( self::$tmp_dir ) ) {
return self::$tmp_dir;
}
if ( ! empty( $dir ) && is_dir( $dir ) && is_writable( $dir ) ) {
$dir = realpath( $dir );
} else {
$dirs = array(
sys_get_temp_dir(),
dirname( $_SERVER['DOCUMENT_ROOT'] ) . '/tmp',
ini_get( 'upload_tmp_dir' ),
'/var/tmp',
'~/tmp',
);
foreach( $dirs as $key => $dir ) {
if( is_dir( $dir ) && is_writable( $dir ) ) {
$dir = realpath( $dir );
break;
}
}
}
if ( ! $dir ) {
throw new OpenSim_Error( 'No writable temporary directory found.' );
}
self::$tmp_dir = $dir;
return $dir;
}
public static function parse_args( $args, $defaults ) {
if( empty( $defaults ) ) {
$defaults = array();
}
if( is_object( $args ) ) {
$args = get_object_vars( $args );
} elseif( is_array( $args ) ) {
$args = $args;
} else {
parse_str( $args, $args );
}
return array_merge( $defaults, $args );
}
public static function connectionstring_to_array( $connectionstring ) {
$parts = explode( ';', $connectionstring );
$creds = array();
foreach ( $parts as $part ) {
$pair = explode( '=', $part );
$creds[ $pair[0] ] = $pair[1] ?? '';
}
if( preg_match( '/:[0-9]+$/', $creds['Data Source'] ) ) {
$host = explode( ':', $creds['Data Source'] );
$creds['Data Source'] = $host[0];
$creds['Port'] = empty( $host[1] || $host[1] == 3306 ) ? null : $creds['Port'];
}
$result = array(
'host' => $creds['Data Source'],
'port' => $creds['Port'] ?? null,
'name' => $creds['Database'],
'user' => $creds['User ID'],
'pass' => $creds['Password'],
);
return $result;
}
// Clone of WP trailingslashit function
public static function trailingslashit( $value ) {
return self::untrailingslashit( $value ) . '/';
}
// Clone of WP untrailingslashit function
public static function untrailingslashit( $value ) {
return rtrim( $value, '/\\' );
}
/**
* Notify user of an error, log it and display it in the admin area
*
* @param mixed $error (string) Error message or (Throwable) Exception
* @param string $type Error severity: 'info', 'warning', 'danger'
* @return void
*/
public static function notify_error( $error, $type = 'warning' ) {
// Initialize the prefix before error type check
$prefix = '[' . strtoupper( $type ) . '] ';
// Retrieve the calling method's information
if ( $error instanceof Throwable ) {
$message = $error->getMessage();
} elseif( is_string($error) ) {
$message = $error;
} else {
$message = _('Unknown error, see log for details');
error_log( $prefix . 'Unidentified error type: ' . gettype( $error ) . ' ' . print_r( $error, true ) );
}
if( ! empty( $message ) ) {
self::notify( $message, $type );
}
}
public static function notify( $message, $type = 'info' ) {
$key = md5( $type . $message ); // Make sure we don't have duplicates
self::$user_notices[$key] = array(
'message' => $message,
'type' => $type,
);
}
public static function get_notices() {
$html = '';
foreach( self::$user_notices as $key => $notice ) {
$type = $notice['type'] ?? 'info';
switch( $type ) {
case 'task-checked':
$html .= sprintf(
'<div class="form-check %s">
<input class="form-check-input" type="checkbox" value="" id="flexCheckChecked" checked readonly>
<label class="form-check-label" for="flexCheckChecked">
%s
</label>
</div>',
$type,
$notice['message']
);
break;
default:
$html .= sprintf(
'<div class="alert alert-%s my-4">%s</div>',
$type,
$notice['message']
);
}
}
return $html;
}
public static function validate_error_type( $type, $fallback = 'light' ) {
$given = $type;
$type = in_array( $type, array(
'primary',
'secondary',
'success',
'danger',
'warning',
'info',
'light',
'dark',
) ) ? $type : $fallback;
return $type;
}
public static function validate_error ( $error, $type = 'light' ) {
if( is_string( $error )) {
$error = array( 'message', $error );
}
$error = self::parse_args( $error, array(
'message' => _('Error'),
'type' => $type,
));
$error['type'] = self::validate_error_type( $error['type'], $type );
return $error;
}
public static function error_html( $error, $type = null ) {
$error = self::validate_error( $error, $type );
$html = sprintf(
'<div class="text-%s">%s</div>',
$error['type'],
$error['message'],
);
return $html;
}
/**
* Log message to error_log, adding calling [CLASS] and function before message, and severity if given
*/
public static function log( $message, $severity = none ) {
$caller = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 );
$caller = $caller[1];
$class = $caller['class'] ?? '';
$function = $caller['function'] ?? '';
$message = sprintf(
'[%s%s%s] %s%s',
$class,
empty( $class ) ? '' : '::',
$function,
empty( $severity ) ? '' : strtoupper( $severity ) . ' ',
$message
);
// Add severity if given
error_log( $message );
}
public static function callback_name_string( $callback ) {
if( is_string( $callback ) ) {
return $callback;
}
if( is_array( $callback ) && is_object( $callback[0] ) ) {
$callback_name = get_class($callback[0]) . '::' . $callback[1];
return $callback_name;
}
if( is_array( $callback ) ) {
return $callback[0] . '::' . $callback[1];
}
if( is_object( $callback ) ) {
return get_class( $callback );
}
return 'Unknown';
}
public static function build_url( $parsed ) {
if( empty( $parsed['host'] ) ) {
$url = '';
} else {
$url = ( $parsed['scheme'] ?? 'https' ) . '://' . $parsed['host'];
}
$url .= $parsed['path'] ?? '';
if( ! empty( $parsed['query'] ) ) {
$url .= '?' . $parsed['query'];
}
return $url;
}
public static function add_query_args( $url, $args ) {
$parsed = parse_url( $url );
$query = $parsed['query'] ?? '';
$query = self::parse_args( $query, array() );
$query = array_merge( $query, $args );
$query = http_build_query( $query );
$parsed['query'] = $query;
$url = self::build_url( $parsed );
return $url;
}
/**
* Basic function to replace WP enqueue_script when not in WP environment.
* Add the script to a private property that will be used with another method to output all scripts.
* Use OSHELPERS_URL constant to build the URL unless it's already full.
* Use self::get_version() to define the version of the script unless it is already defined.
*/
public static function enqueue_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
if( ! file_exists( $src ) ) {
error_log( __FUNCTION__ . ' file not found: ' . $src );
return false;
}
$handle = preg_match( '/^oshelpers-/', $handle ) ? $handle : 'oshelpers-' . $handle;
$handle = ( rtrim ( $handle, '-css' ) ) . '-js';
self::$scripts = self::$scripts ?? array( 'head' => array(), 'footer' => array() );
if( strpos( $src, '://' ) === false ) {
$src = OSHELPERS_URL . ltrim( $src, '/' );
}
$ver = empty( $ver ) ? self::get_version( true ) : self::sanitize_slug( $ver );
$src = self::add_query_args( $src, array( 'ver' => $ver ) );
$section = $in_footer ? 'footer' : 'head';
self::$scripts[$section][$handle] = array(
'src' => $src,
'deps' => $deps,
'ver' => $ver ?? self::get_version( true ),
'in_footer' => $in_footer,
);
}
/**
* Return or output the html for scripts in the head or footer
*
* @param string $section 'head' or 'footer'
* @param bool $echo Output the html if true, return it if false
*/
public static function get_scripts( $section, $echo = false ) {
if( ! isset( self::$scripts[$section] ) ) {
return '';
}
$html = '';
if(empty( self::$scripts[$section] ) ) {
return '';
}
$template = '<script id="%s" src="%s" type="text/javascript"></script>';
$scripts = self::$scripts[$section];
foreach( $scripts as $handle => $script ) {
// error_log( 'Script: ' . print_r( $script, true ) );
$html .= sprintf(
$template,
$handle,
$script['src'],
empty( $script['ver'] ) ? '' : 'version="' . $script['ver'] . '"'
);
}
if( $echo ) {
echo $html;
}
// error_log( $html );
return $html;
}
public static function enqueue_style( $handle, $src, $deps = array(), $ver = false, $media = 'all' ) {
if( ! file_exists( $src ) ) {
error_log( __FUNCTION__ . ' file not found: ' . $src );
return false;
}
$handle = preg_match( '/^oshelpers-/', $handle ) ? $handle : 'oshelpers-' . $handle;
$handle = ( rtrim ( $handle, '-css' ) ) . '-css';
self::$styles = self::$styles ?? array( 'head' => array(), 'footer' => array() );
if( strpos( $src, '://' ) === false ) {
$src = OSHELPERS_URL . ltrim( $src, '/' );
}
$ver = empty( $ver ) ? self::get_version( true ) : self::sanitize_slug( $ver );
$src = self::add_query_args( $src, array( 'ver' => $ver ) );
self::$styles['head'][$handle] = array(
'src' => $src,
'deps' => $deps,
'ver' => $ver,
'media' => $media,
);
}
public static function get_styles( $echo = false ) {
if( ! isset( self::$styles['head'] ) ) {
return '';
}
$html = '';
if(empty( self::$styles['head'] ) ) {
return '';
}
$template = '<link id="%s" rel="stylesheet" href="%s" type="text/css" media="%s">';
$styles = self::$styles['head'];
foreach( $styles as $handle => $style ) {
$html .= sprintf(
$template,
$handle,
$style['src'],
$style['media'],
);
}
if( $echo ) {
echo $html;
}
return $html;
}
public static function sanitize_id( $string ) {
if( empty( $string ) ) {
return false;
}
$id = $string;
try {
$id = transliterator_transliterate("Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove; Lower();", $id );
$id = preg_replace('/[-\s]+/', '-', $id );
} catch ( Error $e ) {
error_log( 'Error sanitizing slug: ' . $e->getMessage() );
$id = $string;
}
return $id;
}
public static function sanitize_slug( $string ) {
return self::sanitize_id( $string );
}
public static function sanitize_url( $url ) {
$url = trim( $url );
$url = str_replace( ' ', '+', $url );
$url = filter_var( $url, FILTER_SANITIZE_URL );
return $url;
}
public static function validate_condition( $condition ) {
if( is_callable( $condition ) ) {
return $condition();
}
if( is_bool( $condition ) ) {
return $condition;
}
switch( $condition ) {
case 'logged_in':
return self::is_logged_in();
case 'logged_out':
return self::is_logged_out();
default:
return false;
}
}
/**
* Get user preferred language from browser settings.
*
* @param bool $long Full locale string if true (en_US), language code otherwise (en)
* @return string
*/
public static function user_locale( $long = true ) {
return $long ? OpenSim_Locale::locale() : OpenSim_Locale::lang();
}
public static function user_lang( $long = false ) {
return $long ? OpenSim_Locale::locale() : OpenSim_Locale::lang();
}
/**
* Return the actual language of the content if localization is setup
*/
public static function content_lang( $long = false ) {
// When localization is setup, we will return user language
// For now, we return english.
$lang = 'en_US';
// return self::user_locale( $long );
return $long ? $lang : substr( $lang, 0, 2 );
}
public static function is_logged_in() {
// WP is not loaded so constants like COOKIEHASH are not available.
// Any cookie matching wordpress_logged_in or wordpress_logged_in_*
// is considered a valid login cookie.
// If logged_in, use first part of cookie value as user_id
foreach( $_COOKIE as $key => $value ) {
if( preg_match( '/^wordpress_logged_in/', $key ) ) {
$parts = explode( '|', $value );
$_SESSION['user_id'] = $parts[0];
break;
}
}
return isset( $_SESSION['user_id'] );
}
public static function is_logged_out() {
return ! self::is_logged_in();
}
public static function get_user_id() {
return $_SESSION['user_id'] ?? false;
}
public static function display_name( $fallback = false ) {
if( ! self::is_logged_in() ) {
return $fallback;
}
// For now, we return the user_id
return self::get_user_id() ?? $fallback;
}
public static function icon( $icon, $size = 'inherit' ) {
if( is_callable( $icon ) ) {
$callback = $icon;
return call_user_func( $callback, $size );
}
$size = is_numeric( $size ) ? $size . 'px' : $size;
return ' <i class="bi bi-' . $icon . '" style="font-size:' . $size . ';"></i> ';
}
/**
* Display small user icon based on in-world Avatar profile picture.
* Not implemented yet. For now, we use bootstrap icons.
* In any case, we don't use gravatar yet.
*/
public static function user_icon( $size = 'inherit' ) {
if( ! self::is_logged_in() ) {
return null;
}
$size = is_numeric( $size ) ? $size . 'px' : $size;
return self::icon( 'person-circle', $size );
// For now, we return user icon with bootstrap library
// return ' <i class="bi bi-person-circle" style="font-size:' . $size . ';"></i> ';
}
/**
* get_option()
*
* Retrieve an option value from
* - $_SESSION['installation']['config'], if exists, organized as an array(
* 'section' => array(
* 'option' => 'value',
* 'option2' => 'value2',
* )
* - self::$config, if exists, organized the same way as $_SESSION['installation']['config']
* - constants defined includes/config.php (map to be implemented later)
* - site configuration data (to be implemented later)
*/
public static function get_option( $option, $default = null ) {
$config = $_SESSION['installation']['config'] ?? self::$config ?? array();
if( empty( $config ) ) {
return $default;
}
// Give value of $config[$section][$key] if when given $option = 'section.key'
if( strpos( $option, '.' ) !== false ) {
$parts = explode( '.', $option );
$section = $config[$parts[0]];
$key = trim($parts[1]);
return $section[$key] ?? $default;
}
// Otherwise return global option
return $config[$option] ?? $default;
}
static function hop( $url = null, $string = null, $format = true ) {
if ( empty( $url ) ) {
// $url = get_option( 'w4os_login_uri' );
return $string;
}
$url = opensim_format_tp( $url, TPLINK_HOP );
if ( ! $format ) {
return $url;
}
$string = ( empty( $string ) ) ? $url : $string;
$classes[] = 'hop';
$classes[] = 'hop-link';
if ( preg_match( ':/app/agent/:', $url ) ) {
$classes[] = 'profile';
}
$string = preg_replace( '+.*://+', '', $string );
return sprintf(
'<a class="%s" href="%s">%s %s</a>',
implode( ' ', $classes ),
$url,
self::icon( 'door-open' ),
$string,
// self::icon( 'box-arrow-up-right' ),
// self::icon( 'arrow-right-square' ),
);
}
public static function is_error( $thing ) {
// Throwable includes Exception and probably other things.
if ( $thing instanceof Throwable ) {
return true;
}
return false;
}
}
$OpenSim = new OpenSim();
$OpenSim->init();
View File
+5
View File
@@ -0,0 +1,5 @@
::placeholder,
input::placeholder {
display: none;
opacity: 0.5;
}
View File
+41
View File
@@ -0,0 +1,41 @@
/*
* Template page CSS
**/
:root {
--bs-primary-rgb: 0, 192, 0;
--bs-secondary-rgb: 63, 159, 63;
}
[data-bs-theme="dark"] {
}
/* Add this to your CSS file */
.dropdown-hover:hover .dropdown-menu {
display: block;
}
.block a,
a.hop-link {
text-decoration: none !important;
}
.cards .accordion-button {
border: 0;
}
.cards .accordion-button:not(.collapsed) {
color: inherit;
background-color: inherit;
box-shadow: none;
}
.cards .accordion-button::after {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>") !important;
transform: rotate(180deg);
}
.accordion-button:not(.collapsed)::after {
color: white;
transform: rotate(0);
}
+17
View File
@@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', () => {
// Function to apply the color scheme
const applyColorScheme = (e) => {
if (e.matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
} else {
document.documentElement.setAttribute('data-bs-theme', 'light');
}
};
// Detect and apply user's preferred color scheme on initial load
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
applyColorScheme(darkModeMediaQuery);
// Listen for changes in the user's color scheme preference
darkModeMediaQuery.addEventListener('change', applyColorScheme);
});
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="<?php echo OpenSim::content_lang() ?? 'en'; ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $page_title; ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
<?php
OpenSim::get_styles( 'head', true );
OpenSim::get_scripts( 'head', true );
?>
</head>
<body class="d-flex flex-column min-vh-100">
<header class="bg-primary text-center mt-auto">
<a class="skip visually-hidden-focusable" href="#main">Skip to main content</a>
<nav class="container navbar navbar-expand-lg">
<?php echo $branding; ?>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbar-header" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<?php echo $main_menu_html; ?>
<?php echo $user_menu_html; ?>
</nav>
</header>
<div class="container-fluid flex-grow-1 p-4">
<!-- <div class="row justify-content-center"> -->
<div class="row justify-content-center">
<?php
// DEBUG
// $sidebar_left = '<div class="card">Card<div>';
// $sidebar_right = '<div class="card">Card<div>';
?>
<main id="main" class="col-lg-auto">
<h1 class="page-title"><?php echo $page_title; ?></h1>
<div class="content text-start">
<?php echo $content; ?>
</div>
</main>
<?php
$sidebar_left = $page->get_sidebar('left');
if ( ! empty( $sidebar_left ) ) :
?>
<aside id="sidebar-left" class="col-lg-4 col-xl-3 sidebar">
<div class="d-grid d-md-flex d-lg-grid gap-4">
<?php echo $sidebar_left; ?>
</aside>
<?php endif; ?>
<?php
$sidebar_right = $page->get_sidebar('right');
if ( ! empty( $sidebar_right ) ) :
?>
<aside id="sidebar-right" class="col-lg-4 col-xl-3 sidebar">
<div class="cards d-grid d-md-flex d-lg-grid gap-4">
<?php echo $sidebar_right; ?>
</div>
</aside>
<?php endif; ?>
</div>
</div>
<footer class="bg-secondary text-center mt-auto">
<nav class="container navbar navbar-expand-lg">
<?php echo $footer; ?>
<?php echo $footer_menu_html; ?>
</nav>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<?php
OpenSim::get_scripts( 'footer', true );
OpenSim::get_styles( 'footer', true );
?>
</body>
</html>
+130
View File
@@ -0,0 +1,130 @@
<?php
$site_title = $site_title ?? 'OpenSimulator Helpers';
$page_title = $page_title ?? 'Unknown page';
$content = $content ?? 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris.';
$sidebar_left = $sidebar_left ?? '';
$sidebar_right = $sidebar_right ?? '';
$version = OpenSim::get_version();
$footer = $footer ?? sprintf( _('OpenSimulator Helpers %s'), $version );
// $menus['main'] = $menu ?? array(
// 'home' => array(
// 'url' => '/',
// 'label' => 'Home',
// ),
// 'about' => array(
// 'url' => '/about',
// 'label' => 'About',
// ),
// );
$menus['user'] = array(
'userprofile' => array(
'url' => '/profile',
'label' => OpenSim::display_name( _('Profile') ),
'icon' => [ 'OpenSim', 'user_icon' ],
'condition' => 'logged_in',
'children' => array(
'account' => array(
'url' => '/account',
'label' => _('Account Settings'),
'icon' => 'sliders'
),
'logout' => array(
'url' => '?action=logout',
'label' => _('Logout'),
'icon' => 'box-arrow-right',
),
),
),
'login' => array(
'url' => '/login',
'label' => 'Login',
'icon' => 'box-arrow-in-right',
'condition' => 'logged_out',
),
);
$menus['footer'] = array(
'github' => array(
'url' => 'http://github.com/magicoli/opensim-helpers',
'label' => 'GitHub Repository',
),
'w4os' => array(
'url' => 'https://w4os.org',
'label' => 'W4OS Project',
),
);
/**
* Build HTML menu using Bootstrap styles
*/
function format_menu( $menu, $slug = 'main', $class = '' ) {
$id = "navbar-$slug";
$html = sprintf(
'<div class="collapse navbar-collapse" id="%s">',
$id
);
$ul_class = "navbar-nav navbar-$slug ms-auto";
$html .= sprintf(
'<ul class="%s">',
$ul_class
);
if( ! is_array( $menu ) ) {
return '';
}
foreach ($menu as $key => $item) {
if (isset($item['condition']) && ! OpenSim::validate_condition($item['condition'])) {
continue;
}
$item_id = "nav-$slug-$key";
if (isset($item['children'])) {
// Add 'dropdown-hover' class for hover functionality
$html .= '<li class="nav-item dropdown dropdown-hover">';
$html .= sprintf(
'<a class="nav-link dropdown-toggle" href="%s" id="%s" role="button" aria-expanded="false">%s%s</a>',
OpenSim::sanitize_url($item['url']),
$item_id,
OpenSim::icon($item['icon']),
strip_tags($item['label']),
);
$html .= '<ul class="dropdown-menu" aria-labelledby="navbarDropdown">';
foreach ($item['children'] as $child_key => $child) {
$child_id = "nav-$slug-$key-$child_key";
$html .= sprintf(
'<li><a class="dropdown-item" id="%s" href="%s">%s%s</a></li>',
$child_id,
OpenSim::sanitize_url($child['url']),
OpenSim::icon($child['icon']),
strip_tags($child['label']),
);
'<li><a class="dropdown-item" href="' . htmlspecialchars($child['url']) . '">' . htmlspecialchars($child['label']) . '</a></li>';
}
$html .= '</ul>';
$html .= '</li>';
} else {
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['label']) . '</a>';
$html .= '</li>';
}
}
$html .= '</ul>';
$html .= '</div>';
return $html;
}
$branding = '<a class="navbar-brand" href="#">' . htmlspecialchars($GLOBALS['site_title']) . '</a>';
// Generate HTML for each menu
$main_menu_html = format_menu( ($menus['main'] ?? null ), 'main' );
$user_menu_html = format_menu( $menus['user'], 'user' );
$footer_menu_html = format_menu( $menus['footer'], 'footer' );
OpenSim::enqueue_script( 'template-page', 'templates/bootstrap.js' );
OpenSim::enqueue_style( 'template-page', 'templates/bootstrap.css' );
require( 'template-page.php' );