mirror of
https://github.com/GuduleLapointe/opensim-helpers.git
synced 2026-08-14 08:52:11 +00:00
merge templates/ and classes/ from 3.x
This commit is contained in:
@@ -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() );
|
||||
}
|
||||
}
|
||||
@@ -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 .= ' ✓';
|
||||
// $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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
) . ' 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
Vendored
@@ -0,0 +1,5 @@
|
||||
::placeholder,
|
||||
input::placeholder {
|
||||
display: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
Vendored
+41
@@ -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);
|
||||
}
|
||||
Vendored
+17
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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' );
|
||||
Reference in New Issue
Block a user