1
0
Fork 0

Compare commits

...

22 Commits

Author SHA1 Message Date
Anonymous Contributor 685853a9f9 Make daily cronjobs run at midnight 2023-09-15 19:20:06 +02:00
Anonymous Contributor 36ef1c5721 Add documentation 2023-09-15 19:19:45 +02:00
Anonymous Contributor 882c5ae372 Fix icon url in presession template 2023-09-15 19:03:26 +02:00
Anonymous Contributor e4e050908b Mention IAR password to user 2023-09-11 08:50:33 +02:00
Anonymous Contributor 2b5c00db1c Improve design of profile page 2023-09-11 08:31:21 +02:00
Anonymous Contributor 2f9caf8923 Show IAR creation state/download on profile page 2023-09-11 08:29:28 +02:00
Anonymous Contributor 939f600b3f Use curl for getDataFromHTTP utility function 2023-09-11 08:01:12 +02:00
Anonymous Contributor 8231fbb08c Fix DB query and stats parsing of RegionChecker 2023-09-11 07:29:02 +02:00
Anonymous Contributor eb5a850869 Small fixes 2023-09-11 07:05:21 +02:00
Anonymous Contributor ce41e107af Fix documentation in example config 2023-09-11 06:06:33 +02:00
Anonymous Contributor 204fcb96e1 Fix identities table not being migrated properly 2023-09-11 06:06:22 +02:00
Anonymous Contributor 891f1dff70 Add API endpoint to make IAR files downloadable 2023-09-11 06:05:50 +02:00
Anonymous Contributor 6a4c343b03 Optimize IarMonitor 2023-09-11 06:04:30 +02:00
Anonymous Contributor dbcb57f71f Add better explanations for the identity system 2023-09-11 04:26:06 +02:00
Anonymous Contributor 1a869b62c2 Create separate user profile for new identities 2023-09-11 03:13:59 +02:00
Anonymous Contributor 93d8aafd6f Add new cronjob for IAR request queue processing 2023-09-10 12:07:45 +02:00
Anonymous Contributor 2ed8285c93 Change structure of IAR table, add migrations 2023-09-10 12:00:49 +02:00
Anonymous Contributor a166e65421 Return JSON report for cron calls 2023-09-10 11:58:20 +02:00
Anonymous Contributor e6ba73430b Add simple OpenSimulator RestConsole client 2023-09-10 07:41:42 +02:00
Anonymous Contributor 80db5f35d1 Add internal cronjob for PHP session cleanup 2023-09-10 04:53:40 +02:00
Anonymous Contributor bb40c8c9a4 Port working cronjobs to new CronJob API 2023-09-10 04:53:13 +02:00
Anonymous Contributor a213a38b3c Add simple API and starter for cron jobs 2023-09-10 04:41:21 +02:00
35 changed files with 907 additions and 367 deletions

View File

@ -1,2 +1,41 @@
# OpenSim-Gridverwaltung
# MCP: OpenSim-Gridverwaltung
Das MCP ist ein PHP-Webinterface für Benutzer und Administratoren von OpenSimulator-Grids. Es ermöglicht Benutzern die Registrierung (auf Einladung) und Verwaltung des eigenen OpenSimulator-Accounts im Self-Service-Verfahren. Administratoren können Accounts und Regionen einfacher verwalten.
## Installation
Voraussetzung ist, dass die Datenbankstruktur eines OpenSimulator-Grids bereits existiert. Das MCP muss vor der Nutzung mit den Zugangsdaten derselben Datenbank konfiguriert werden. Eigene Tabellen des MCP besitzen zur Vermeidung von Konflikten den Präfix `mcp_`.
Folgende PHP-Erweiterungen werden benötigt:
1. php-curl
2. php-mysql (PDO)
3. php-xml
4. php-xmlrpc
Für bessere Performance kann optional `php-apcu` installiert werden.
Die Installation läuft folgendermaßen ab:
1. Gewünschtes Release als ZIP/TAR-Archiv oder per `git clone` herunterladen
2. Verzeichnisse `app`, `data`, `lib`, `public` und `templates` in das Verzeichnis des Webservers entpacken
3. Öffentliche Stammverzeichnis des Webservers (Apache: `DocumentRoot`, nginx: `root`) auf Pfad zum Verzeichnis `public` ändern
4. Index des Webservers auf index.php ändern, falls erforderlich
5. Beispielkonfiguration `config.example.ini` anpassen, in `config.ini` umbenennen und in das Verzeichnis der in Schritt 2 entpackten Verzeichnisse verschieben
## Aktualisierung
Zur Aktualisierung müssen lediglich die Verzeichnisse `app`, `lib`, `public` und `templates`, sowie der Inhalt von `data` (außer `iars`) ersetzt werden.
Die Migration der Datenbankstruktur erfolgt automatisch. Möglicherweise erforderliche Änderungen an der Konfiguration sind den Release-Informationen zu entnehmen.
## Entwickler
Die Abhängigkeiten des Frontends werden über [npm](https://www.npmjs.com/) verwaltet. Alle erforderlichen Pakete können nach dem Download des Repository über `npm install` heruntergeladen werden.
In `package.json` ist zudem ein Buildprozess (Befehl: `npm build`) definiert, durch den die Sass-Stylesheets im Verzeichnis `scss` kompiliert und optimiert und zusammen mit Schriftarten und Skripts im öffentlichen Webverzeichnis abgelegt werden.
Das Backend besitzt kein Dependency Management. Die einzige Abhängigkeit, [PHPMailer](https://github.com/PHPMailer/PHPMailer), wurde manuell in das Repository eingefügt. Der Autoloader sucht im Verzeichnis `lib` nach solchen externen Klassen.
### Verarbeitung von Anfragen
Das Skript `index.php` wird bei allen Anfragen aufgerufen. Die angeforderte Route wird als GET-Parameter `<Gruppe>=<Name>` übermittelt. Gruppen (momentan `api` und `page`), zugehörige Seiten und die assoziierte `RequestHandler`-Subklasse sind in `Mcp.php` definiert.
Ist zu einer Anfrage eine Route definiert, wird der zugehörige `RequestHandler` erzeugt. Ist eine `Middleware`-Klasse für diesen definiert, ist die weitere Verarbeitung von dem Rückgabewert von `Middleware::canAccess()` abhängig.
Schließlich wird je nach Methode der Anfrage `RequestHandler::get()` bzw. `RequestHandler::post()` aufgerufen.

View File

@ -11,6 +11,7 @@ class Mcp implements ConnectionProvider
private ?PDO $db = null;
private array $config;
private string $dataDir;
private string $templateDir;
const ROUTES = [
@ -21,7 +22,8 @@ class Mcp implements ConnectionProvider
'getAccessList' => 'Api\\GetAccessList',
'onlineDisplay' => 'Api\\OnlineDisplay',
'viewerWelcomeSite' => 'Api\\ViewerWelcomePage',
'runCron' => 'Api\\CronStarter'
'runCron' => 'Api\\CronStarter',
'downloadIar' => 'Api\\DownloadIar'
],
'page' => [
'dashboard' => 'Page\\Dashboard',
@ -42,6 +44,7 @@ class Mcp implements ConnectionProvider
public function __construct($basedir)
{
$this->templateDir = $basedir.DIRECTORY_SEPARATOR.'templates';
$this->dataDir = $basedir.DIRECTORY_SEPARATOR.'data';
$this->config = array();
try {
$config = parse_ini_file($basedir.DIRECTORY_SEPARATOR.'config.ini', true);
@ -64,6 +67,9 @@ class Mcp implements ConnectionProvider
}
}
/**
* Connects to the MySQL database (if not done already) and returns the connection.
*/
public function db(): PDO
{
if ($this->db == null) {
@ -75,16 +81,29 @@ class Mcp implements ConnectionProvider
return $this->db;
}
/**
* Returns the value associated with the specified key in this config, as either a string, an integer or an array.
* Keys are lower-cased for compatibility reasons.
*
* If there is no entry with this key, an empty array is returned.
*/
public function config($key): string|array|int
{
return $this->config[strtolower($key)];
$realKey = strtolower($key);
return isset($this->config[$realKey]) ? $this->config[$realKey] : array();
}
/**
* Returns a hidden form field with the current CSRF token set.
*/
public function csrfField(): string
{
return '<input type="hidden" name="csrf" value="'.(isset($_SESSION['csrf']) ? $_SESSION['csrf'] : '').'">';
}
/**
* Creates a TemplateBuilder instance for the specified template file, setting some basic variables.
*/
public function template($name): TemplateBuilder
{
return (new TemplateBuilder($this->templateDir, $name))->vars([
@ -94,6 +113,14 @@ class Mcp implements ConnectionProvider
])->unsafeVar('csrf', $this->csrfField());
}
/**
* Returns the path of the data/ directory, mostly used for dynamically created assets.
*/
public function getDataDir(): string
{
return $this->dataDir;
}
public function handleRequest()
{
$reqClass = 'Mcp\\Page\\Error';

View File

@ -14,6 +14,8 @@ class MigrationManager
'CREATE TABLE IF NOT EXISTS `mcp_invites` (`InviteCode` CHAR(64) NOT NULL, PRIMARY KEY (`InviteCode`)) ENGINE InnoDB',
'CREATE TABLE IF NOT EXISTS `mcp_offlineim_send` (`id` int(6) NOT NULL DEFAULT 0) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci',
'CREATE TABLE IF NOT EXISTS `mcp_regions_info` (`regionID` CHAR(36) NOT NULL COLLATE utf8_unicode_ci, `RegionVersion` VARCHAR(128) NOT NULL DEFAULT "" COLLATE utf8_unicode_ci, `ProcMem` INT(11) NOT NULL, `Prims` INT(11) NOT NULL, `SimFPS` INT(11) NOT NULL, `PhyFPS` INT(11) NOT NULL, `OfflineTimer` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`regionID`) USING BTREE) COLLATE=utf8_unicode_ci ENGINE=InnoDB',
'CREATE TABLE IF NOT EXISTS `mcp_cron_runs` (`Name` VARCHAR(50) NOT NULL, `LastRun` INT(11) UNSIGNED NOT NULL, PRIMARY KEY(`Name`)) ENGINE InnoDB',
'CREATE TABLE IF NOT EXISTS `mcp_iar_state` (`userID` CHAR(36) NOT NULL COLLATE utf8_unicode_ci, `filesize` BIGINT(20) NOT NULL DEFAULT 0, `iarfilename` VARCHAR(64) NOT NULL COLLATE utf8_unicode_ci, `state` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0, `created` INT(11) UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`userID`) USING BTREE) COLLATE=utf8_unicode_ci ENGINE=InnoDB',
'CREATE TRIGGER IF NOT EXISTS del_id_trig AFTER DELETE ON UserAccounts FOR EACH ROW DELETE FROM mcp_user_identities WHERE mcp_user_identities.PrincipalID = OLD.PrincipalID OR mcp_user_identities.IdentityID = OLD.PrincipalID',
'CREATE TRIGGER IF NOT EXISTS del_pwres_trig AFTER DELETE ON UserAccounts FOR EACH ROW DELETE FROM mcp_password_reset WHERE mcp_password_reset.PrincipalID = OLD.PrincipalID'
];
@ -23,12 +25,20 @@ class MigrationManager
'RENAME TABLE IF EXISTS UserIdentitys TO mcp_user_identities, PasswordResetTokens TO mcp_password_reset, InviteCodes TO mcp_invites, im_offline_send TO mcp_offlineim_send, regions_info TO mcp_regions_info',
'ALTER TABLE mcp_invites MODIFY COLUMN InviteCode CHAR(64) NOT NULL',
'ALTER TABLE mcp_regions_info MODIFY COLUMN regionID CHAR(36), MODIFY COLUMN ProcMem INT(11) UNSIGNED NOT NULL, MODIFY COLUMN Prims INT(11) UNSIGNED NOT NULL, MODIFY COLUMN SimFPS FLOAT NOT NULL, MODIFY COLUMN PhyFPS FLOAT NOT NULL, MODIFY COLUMN OfflineTimer BIGINT UNSIGNED NOT NULL DEFAULT 0',
'ALTER TABLE mcp_user_identities MODIFY COLUMN PrincipalID CHAR(36) COLLATE utf8mb3_general_ci, MODIFY COLUMN IdentityID CHAR(36) COLLATE utf8mb3_general_ci',
'CREATE TRIGGER IF NOT EXISTS del_id_trig AFTER DELETE ON UserAccounts FOR EACH ROW DELETE FROM mcp_user_identities WHERE mcp_user_identities.PrincipalID = OLD.PrincipalID OR mcp_user_identities.IdentityID = OLD.PrincipalID',
'CREATE TRIGGER IF NOT EXISTS del_pwres_trig AFTER DELETE ON UserAccounts FOR EACH ROW DELETE FROM mcp_password_reset WHERE mcp_password_reset.PrincipalID = OLD.PrincipalID'
],
2 => [
'CREATE TABLE IF NOT EXISTS `mcp_cron_runs` (`Name` VARCHAR(50) NOT NULL, `LastRun` INT(11) UNSIGNED NOT NULL, PRIMARY KEY(`Name`)) ENGINE InnoDB'
],
3 => [
'RENAME TABLE IF EXISTS iarstates TO mcp_iar_state',
'ALTER TABLE mcp_iar_state MODIFY COLUMN userID CHAR(36) NOT NULL COLLATE utf8_unicode_ci, DROP COLUMN running, ADD COLUMN `state` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 AFTER iarfilename, ADD COLUMN created INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `state`'
]
];
private const MIGRATE_VERSION_CURRENT = 2;
private const MIGRATE_VERSION_CURRENT = 4;
private int $migrateVersion;
private string $migrateVersionFile;

View File

@ -246,6 +246,9 @@ class OpenSim
$statementDelete = $this->pdo->prepare('DELETE FROM UserAccounts WHERE PrincipalID = ?');
$statementDelete->execute([$identId]);
$statementUserProfile = $this->pdo->prepare('DELETE FROM userprofile WHERE useruuid = ?');
$statementUserProfile->execute([$identId]);
return true;
}

View File

@ -19,12 +19,20 @@ class TemplateBuilder
$this->name = $name;
}
/**
* Sets another template to be the "parent" of this one.
*
* The template specified in this TemplateBuilder's constructor will be included into the parent.
*/
public function parent(string $parent): TemplateBuilder
{
$this->parent = $parent;
return $this;
}
/**
* Sets multiple variables after escaping them.
*/
public function vars(array $vars): TemplateBuilder
{
foreach ($vars as $key => $val) {
@ -33,18 +41,29 @@ class TemplateBuilder
return $this;
}
/**
* Sets the specified variable for this template, after escaping it.
*/
public function var(string $key, string $val): TemplateBuilder
{
$this->vars[$key] = htmlspecialchars($val);
return $this;
}
/**
* Sets the specified variable for this template WITHOUT escaping it.
*
* User input included this way has to be manually sanitized before.
*/
public function unsafeVar(string $key, string $val): TemplateBuilder
{
$this->vars[$key] = $val;
return $this;
}
/**
* Displays the template(s) with the current set of variables.
*/
public function render(): void
{
$v = new TemplateVarArray($this->vars);

50
app/api/CronStarter.php Normal file
View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mcp\Api;
use Mcp\RequestHandler;
class CronStarter extends RequestHandler
{
private const CRONJOBS_INTERNAL = ['SessionCleanup'];
public function get(): void
{
if ($this->app->config('cron-restriction') == 'key' && !(isset($_GET['key']) && hash_equals($this->app->config('cron-key'), $_GET['key']))) {
http_response_code(403);
return;
}
$cronJobs = array_merge($this->app->config('cronjobs'), $this::CRONJOBS_INTERNAL);
$cronStatement = $this->app->db()->prepare('SELECT Name,LastRun FROM mcp_cron_runs');
$cronStatement->execute();
$jobRuns = array();
while ($row = $cronStatement->fetch()) {
$jobRuns[$row['Name']] = $row['LastRun'];
}
$resArray = [];
$cronUpdateStatement = $this->app->db()->prepare('REPLACE INTO mcp_cron_runs(Name,LastRun) VALUES (?,?)');
foreach ($cronJobs as $jobName) {
$jobClass = "Mcp\\Cron\\".$jobName;
if (in_array($jobName, $cronJobs)) {
$job = (new $jobClass($this->app));
$now = time();
$nextRun = $job->getNextRun(isset($jobRuns[$jobName]) ? $jobRuns[$jobName] : $now - 60);
if ($now >= $nextRun && $job->run()) {
$cronUpdateStatement->execute([$jobName, time()]);
$resArray[$jobName] = ['result' => 'ok', 'nextRun' => $job->getNextRun(time())];
}
else {
$resArray[$jobName] = ['result' => 'failed'];
}
}
}
echo json_encode($resArray);
}
}

24
app/api/DownloadIar.php Normal file
View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Mcp\Api;
class DownloadIar extends \Mcp\RequestHandler
{
public function get(): void
{
if (preg_match('/^[a-f0-9]{32}$/', $_GET['id'])) {
$path = $this->app->getDataDir().DIRECTORY_SEPARATOR.'iars'.DIRECTORY_SEPARATOR.$_GET['id'].'.iar';
if (file_exists($path)) {
header('Content-Type: '.mime_content_type($path));
header('Content-Disposition: attachment; filename='.$_GET['id'].'.iar');
header('Content-Length: '.filesize($path));
readfile($path);
return;
}
}
http_response_code(404);
}
}

51
app/cron/AssetChecker.php Normal file
View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
class AssetChecker extends CronJob
{
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::MONTHLY);
}
public function run(): bool
{
$statement = $this->app->db()->prepare("SELECT id,hash FROM fsassets ORDER BY create_time DESC");
$statement->execute();
$count = 0;
while ($row = $statement->fetch()) {
$fileNameParts = array();
$fileNameParts[0] = substr($row['hash'], 0, 2);
$fileNameParts[1] = substr($row['hash'], 2, 2);
$fileNameParts[2] = substr($row['hash'], 4, 2);
$fileNameParts[3] = substr($row['hash'], 6, 4);
$fileNameParts[4] = $row['hash'] . ".gz";
$fileNameParts['UUID'] = $row['id'];
$fileNameParts['FilePath'] = "/data/assets/base/" . $fileNameParts[0] . "/" . $fileNameParts[1] . "/" . $fileNameParts[2] . "/" . $fileNameParts[3] . "/" . $fileNameParts[4];
if (file_exists($fileNameParts['FilePath'])) {
$filesize = filesize($fileNameParts['FilePath']);
if ($filesize === false) {
continue;
}
} else {
$filesize = 0;
}
$fileNameParts['FileSize'] = $filesize;
$fileNameParts['Count'] = $count++;
if ($fileNameParts['FileSize'] == 0) {
$add = $this->app->db()->prepare('DELETE FROM fsassets WHERE hash = :fileHash');
$add->execute(['fileHash' => $row['hash']]);
}
}
return true;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
class CheckInventory extends CronJob
{
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::MONTHLY);
}
public function run(): bool
{
$invCheckStatement = $this->app->db()->prepare("UPDATE inventoryitems i SET i.inventoryName = concat('[DEFEKT] ', i.inventoryName)
WHERE i.assetID IN (
SELECT i.assetID FROM inventoryitems i WHERE
NOT EXISTS(
SELECT * FROM fsassets fs WHERE fs.id = i.assetID)
AND NOT i.inventoryName LIKE '[DEFEKT] %' AND i.assetType <> 24
)");
$invCheckStatement->execute();
return true;
}
}

52
app/cron/CronJob.php Normal file
View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
use DateInterval;
use DateTime;
use Mcp\Mcp;
abstract class CronJob {
protected Mcp $app;
private Frequency $freq;
public function __construct(Mcp $app, Frequency $freq)
{
$this->app = $app;
$this->freq = $freq;
}
public function getNextRun(int $lastRun)
{
$prevDate = getdate($lastRun);
$res = new DateTime('@'.$lastRun);
switch($this->freq) {
case Frequency::EACH_MINUTE:
$res->add(DateInterval::createFromDateString('1 minute'));
break;
case Frequency::HOURLY:
$res->add(DateInterval::createFromDateString('1 hour'));
break;
case Frequency::DAILY:
$res->add(DateInterval::createFromDateString('1 day'));
$res->setTime(0, 0, 0);
break;
case Frequency::WEEKLY:
$res->add(DateInterval::createFromDateString('1 week'));
break;
case Frequency::MONTHLY:
$res->setDate($prevDate['year'] + ($prevDate['mon'] == 12 ? 1 : 0), $prevDate['mon'] == 12 ? 1 : $prevDate['mon'] + 1, 1);
break;
case Frequency::YEARLY:
$res->setDate($prevDate['year'] + 1, 1, 1);
break;
default: break;
}
return $res->getTimestamp();
}
abstract public function run(): bool;
}

14
app/cron/Frequency.php Normal file
View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
enum Frequency
{
case YEARLY; // 01.01. of each year
case MONTHLY; // 1st of each month
case WEEKLY; // 1 week after last run
case DAILY; // Next day after last run, at 00:00
case HOURLY; // One hour after last run
case EACH_MINUTE; // One minute after last run
}

103
app/cron/IarMonitor.php Normal file
View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
use Mcp\OpenSim;
use Mcp\Opensim\RestConsole;
use Mcp\Util\Util;
class IarMonitor extends CronJob
{
private ?RestConsole $console = null;
private bool $consoleAvailable = true;
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::EACH_MINUTE);
}
public function run(): bool
{
$opensim = new OpenSim($this->app->db());
$dirPath = $this->app->getDataDir().DIRECTORY_SEPARATOR.'iars';
if (!is_dir($dirPath)) {
mkdir($dirPath);
}
$statement = $this->app->db()->prepare("SELECT userID,iarfilename,filesize,state FROM mcp_iar_state WHERE state < ?");
$statement->execute([2]);
if ($statement->rowCount() > 0) {
while ($row = $statement->fetch()) {
if ($row['state'] == 0) { // 0 - Request to OS pending
if ($this->console() === false) {
continue;
}
$name = explode(' ', $opensim->getUserName($row['userID']));
if ($this->console()->sendCommand('save iar '.$name[0].' '.$name[1].' /* password '.$this->app->config('iarfetcher')['os-iar-path'].$row['iarfilename'])) {
$statementUpdate = $this->app->db()->prepare('UPDATE mcp_iar_state SET state = ? WHERE userID = ?');
$statementUpdate->execute([1, $row['userID']]);
}
} elseif ($row['state'] == 1) { // 1 - IAR Creation in progress
$fullFilePath = $dirPath.DIRECTORY_SEPARATOR.$row['iarfilename'];
if (file_exists($fullFilePath)) {
$filesize = filesize($fullFilePath);
if ($filesize != $row['filesize']) {
$statementUpdate = $this->app->db()->prepare('UPDATE mcp_iar_state SET filesize = ? WHERE userID = ?');
$statementUpdate->execute([$filesize, $row['userID']]);
} else {
$statementUpdate = $this->app->db()->prepare('UPDATE mcp_iar_state SET state = ?, created = ? WHERE userID = ?');
$statementUpdate->execute([2, time(), $row['userID']]);
Util::sendInworldIM("00000000-0000-0000-0000-000000000000", $row['userID'], "Inventory", $this->app->config('grid')['homeurl'], "Deine IAR ist fertig zum Download: https://".$this->app->config('domain').'/index.php?api=downloadIar&id='.substr($row['iarfilename'], 0, strlen($row['iarfilename']) - 4).' , sie ist mit dem Passwort "password" geschützt.');
}
}
}
}
if ($this->console != null) {
$this->console->closeSession();
}
}
// 2 - IAR creation finished; delete if expired
$weekOld = time() - 604800;
$statementExpired = $this->app->db()->prepare('SELECT userID,iarfilename FROM mcp_iar_state WHERE state = ? AND created < ?');
$statementExpired->execute([2, $weekOld]);
$statementDeleteExpired = $this->app->db()->prepare('DELETE FROM mcp_iar_state WHERE state = ? AND userID = ?');
while ($row = $statementExpired->fetch()) {
$fullFilePath = $dirPath.DIRECTORY_SEPARATOR.$row['iarfilename'];
if (file_exists($fullFilePath) && unlink($fullFilePath)) {
$statementDeleteExpired->execute([2, $row['userID']]);
}
}
return true;
}
private function console(): RestConsole|bool
{
if (!$this->consoleAvailable) {
return false;
}
if ($this->console == null) {
$restCfg = $this->app->config('iarfetcher');
$console = new RestConsole($restCfg['host'], intval($restCfg['port']));
if ($console->startSession($restCfg['user'], $restCfg['password'])) {
$this->console = $console;
}
else {
$this->consoleAvailable = false;
return false;
}
}
return $this->console;
}
}

89
app/cron/OfflineIm.php Normal file
View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
use Mcp\OpenSim;
use Mcp\Util\SmtpClient;
use SimpleXMLElement;
class OfflineIm extends CronJob
{
private const IM_TYPE = array(
"0" => "eine Nachricht",
"3" => "eine Gruppeneinladung",
"4" => "ein Inventaritem",
"5" => "eine Bestätigung zur Annahme von Inventar",
"6" => "eine Information zur Ablehnung von Inventar",
"7" => "eine Aufforderung zur Gruppenwahl",
"9" => "ein Inventaritem von einem Script",
"19" => "eine Nachricht von einem Script",
"32" => "eine Gruppennachricht",
"38" => "eine Freundschaftsanfrage",
"39" => "eine Bestätigung über die Annahme der Freundschaft",
"40" => "eine Information über das Ablehnen der Freundschaft"
);
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::EACH_MINUTE);
}
public function run(): bool
{
$statement = $this->app->db()->prepare("SELECT ID,PrincipalID,Message FROM im_offline");
$statement->execute();
while ($row = $statement->fetch()) {
$opensim = new OpenSim($this->app->db());
$email = $opensim->getUserMail($row['PrincipalID']);
$allowOfflineIM = $opensim->allowOfflineIM($row['PrincipalID']);
if ($email != "" && $allowOfflineIM == "TRUE" && !$this->isMailAlreadySent($row['ID'])) {
$statementSend = $this->app->db()->prepare('INSERT INTO mcp_offlineim_send (id) VALUES (:idnummer)');
$statementSend->execute(['idnummer' => $row['ID']]);
$mailcfg = $this->app->config('smtp');
$smtpClient = new SmtpClient($mailcfg['host'], intval($mailcfg['port']), $mailcfg['address'], $mailcfg['password']);
$xmlMessage = new SimpleXMLElement($row['Message']);
$imType = $this::IM_TYPE["" . $xmlMessage->dialog . ""];
$htmlMessage = "Du hast " . $imType . " in " . $this->app->config('grid')['name'] . " bekommen. <br><p><ul><li>" . htmlspecialchars($xmlMessage->message) . "</li></ul></p>Gesendet von: ";
if (isset($xmlMessage->fromAgentName)) {
$htmlMessage .= $xmlMessage->fromAgentName;
}
if (isset($xmlMessage->RegionID) && isset($xmlMessage->Position)) {
if ($xmlMessage->Position->X != 0 || $xmlMessage->Position->Y != 0 || $xmlMessage->Position->Z != 0) { //TODO
$htmlMessage .= " @ " . $opensim->getRegionName($xmlMessage->RegionID) . "/" . $xmlMessage->Position->X . "/" . $xmlMessage->Position->Y . "/" . $xmlMessage->Position->Z;
} else {
$htmlMessage .= " @ " . $opensim->getRegionName($xmlMessage->RegionID);
}
}
$tpl = $this->app->template('mail.php')->vars([
'title' => substr($imType, strpos($imType, ' '))
])->unsafeVar('message', $htmlMessage);
$smtpClient->sendHtml($mailcfg['address'], $mailcfg['name'], $email, "Du hast " . $imType . " in " . $this->app->config('grid')['name'] . ".", $tpl);
}
}
return true;
}
private function isMailAlreadySent($id): bool
{
$statement = $this->app->db()->prepare("SELECT 1 FROM mcp_offlineim_send WHERE id = ? LIMIT 1");
$statement->execute(array($id));
if ($statement->rowCount() != 0) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
use Mcp\OpenSim;
use Mcp\Util\Util;
class RegionChecker extends CronJob
{
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::DAILY);
}
public function run(): bool
{
$statement = $this->app->db()->prepare("SELECT uuid,regionName,owner_uuid,serverURI,OfflineTimer FROM regions LEFT JOIN mcp_regions_info ON regions.uuid = mcp_regions_info.regionID COLLATE utf8mb3_unicode_ci");
$statement->execute();
while ($row = $statement->fetch()) {
$curl = curl_init($row['serverURI'] . 'jsonSimStats');
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
if ($result === false || strlen($result) == 0) {
if ($row['OfflineTimer'] == null) {
continue;
}
$opensim = new OpenSim($this->app->db());
$longOffline = ($row['OfflineTimer'] + 3600) <= time();
echo "Die Region " . $row['regionName'] . " von " . $opensim->getUserName($row['owner_uuid']) . " ist " . ($longOffline ? "seit über einer Stunde" : "") . " nicht erreichbar.\n"; //TODO: Increase to 1-3 months
if ($longOffline) {
if ($this->app->config('grid')['delete-inactive-regions']) {
Util::sendInworldIM("00000000-0000-0000-0000-000000000000", $row['owner_uuid'], "Region", $this->app->config('grid')['homeurl'], "WARNUNG: Deine Region '" . $row['regionName'] . "' ist nicht erreichbar und wurde deshalb aus dem Grid entfernt.");
$statementUpdate = $this->app->db()->prepare('DELETE FROM regions WHERE uuid = :uuid');
$statementUpdate->execute(['uuid' => $row['uuid']]);
}
$statementRemoveStats = $this->app->db()->prepare('DELETE FROM mcp_regions_info WHERE regionID = :uuid');
$statementRemoveStats->execute(['uuid' => $row['uuid']]);
} elseif ($this->app->config('grid')['warn-inactive-regions']) {
Util::sendInworldIM("00000000-0000-0000-0000-000000000000", $row['owner_uuid'], "Region", $this->app->config('grid')['homeurl'], "WARNUNG: Deine Region '" . $row['regionName'] . "' ist nicht erreichbar!");
}
} else {
$regionData = json_decode($result);
$statementAccounts = $this->app->db()->prepare('REPLACE INTO `mcp_regions_info` (`regionID`, `RegionVersion`, `ProcMem`, `Prims`, `SimFPS`, `PhyFPS`, `OfflineTimer`) VALUES (:regionID, :RegionVersion, :ProcMem, :Prims, :SimFPS, :PhyFPS, :OfflineTimer)');
$statementAccounts->execute(['regionID' => $row['uuid'], 'RegionVersion' => $regionData->Version, 'ProcMem' => intval(str_replace(',', '', $regionData->ProcMem)), 'Prims' => $regionData->Prims, 'SimFPS' => $regionData->SimFPS, 'PhyFPS' => $regionData->PhyFPS, 'OfflineTimer' => time()]);
}
}
return true;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Mcp\Cron;
class SessionCleanup extends CronJob
{
public function __construct(\Mcp\Mcp $app)
{
parent::__construct($app, Frequency::HOURLY);
}
public function run(): bool
{
error_log("Session GC");
session_start();
session_gc();
return true;
}
}

View File

@ -3,8 +3,20 @@ declare(strict_types=1);
namespace Mcp\Middleware;
/**
* Middleware implementations can be registered for a RequestHandler.
*
* If this is done, request processing continues only if Middleware::canAccess() returns true.
*/
interface Middleware
{
/**
* Returns true if the request should be processed, i.e. if the client has permissionn to perform this request.
*/
public function canAccess(): bool;
/**
* Called when canAcces() returns false, e.g. to redirect unauthorized users.
*/
public function handleUnauthorized(): void;
}
}

133
app/opensim/RestConsole.php Normal file
View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Mcp\Opensim;
use XMLReader;
class RestConsole
{
private string $baseUrl;
private ?string $sessionId;
public function __construct(string $host, int $port)
{
$this->baseUrl = "http://".$host.':'.$port;
}
public function startSession(string $username, string $password): bool
{
$response = $this->sendRequest('/StartSession/', [
'USER' => $username,
'PASS' => $password
]);
if ($this->detectError($response, '/StartSession/')) {
return false;
}
$this->sessionId = $response['SessionID'];
$this->readResponses();
return true;
}
public function closeSession(): void
{
$response = $this->sendRequest('/CloseSession/', [
'ID' => $this->sessionId
]);
$this->detectError($response, '/CloseSession/');
}
public function readResponses(): array
{
if ($this->sessionId == null) {
return array();
}
$response = $this->sendRequest('/ReadResponses/'.$this->sessionId);
if ($this->detectError($response, '/ReadResponses/')) {
return array();
}
return $response['Line'];
}
public function sendCommand(string $command): bool
{
$response = $this->sendRequest('/SessionCommand/', [
'ID' => $this->sessionId,
'COMMAND' => $command
]);
if ($this->detectError($response, '/SessionCommand/')) {
return false;
}
return $response['Result'] == 'OK';
}
private function detectError(array|int $response, string $request): bool
{
if (gettype($response) == 'integer') {
error_log('OS RestConsole request '.$this->baseUrl.$request.' failed, status: '.$response);
return true;
}
return false;
}
private function sendRequest(string $request, array $data = array()): array|int
{
$curl = curl_init($this->baseUrl.$request);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'mcp-restconsole/0.0.1');
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Accept' => 'text/xml'
]);
$postData = '';
foreach ($data as $key => $val) {
$postData = $postData.(strlen($postData) > 0 ? '&' : '').$key.'='.$val;
}
curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
$res = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status != 200) {
return $status;
}
return $this->parseXml($res);
}
private function parseXml(string $response): array
{
$xmlReader = XMLReader::XML($response, "UTF-8");
$res = array();
if ($xmlReader->next()) {
$consoleSession = $xmlReader->expand();
if ($consoleSession->nodeName == 'ConsoleSession') {
foreach ($consoleSession->childNodes as $childNode) {
$name = $childNode->nodeName;
if (isset($res[$name])) {
if (gettype($res[$name]) == 'string') {
$res[$name] = array($res[$name], $childNode->nodeValue);
}
else {
$res[$name][] = $childNode->nodeValue;
}
}
else {
$res[$name] = $childNode->nodeValue;
}
}
}
}
return $res;
}
}

View File

@ -11,7 +11,7 @@ use Mcp\Util\Util;
class ForgotPassword extends \Mcp\RequestHandler
{
const MESSAGE = 'Hallo %%NAME%%,<br/><br/>wir haben soeben eine Anfrage zur Zurücksetzung des Passworts für deinen 4Creative-Account erhalten.<br/><br/>Klicke <a href="%%RESET_LINK%%">hier</a>, um ein neues Passwort festzulegen. Dieser Link läuft in 24 Stunden ab.<br/><br/>Falls du diese Anfrage nicht gesendet hast, ignoriere sie einfach. Bei weiteren Fragen kannst du uns unter info@4creative.net oder per Discord über @ikeytan erreichen.';
const MESSAGE = 'Hallo %%NAME%%,<br/><br/>wir haben soeben eine Anfrage zur Zurücksetzung des Passworts für deinen 4Creative-Account erhalten.<br/><br/>Klicke <a href="%%RESET_LINK%%">hier</a>, um ein neues Passwort festzulegen. Dieser Link läuft in 24 Stunden ab.<br/><br/>Falls du diese Anfrage nicht gesendet hast, ignoriere sie einfach. Bei weiteren Fragen kannst du uns unter info@4creative.net oder über unseren Discord-Server erreichen.';
public function __construct(\Mcp\Mcp $app)
{

View File

@ -30,12 +30,11 @@ class Identities extends \Mcp\RequestHandler
$opensim = new OpenSim($this->app->db());
$csrf = $this->app->csrfField();
while ($row = $statement->fetch()) {
if ($row['IdentityID'] == $_SESSION['UUID']) {
$entry = '<tr><td>'.htmlspecialchars(trim($opensim->getUserName($row['IdentityID']))).' <span class="badge badge-info">Aktiv</span></td><td>-</td></tr>';
} else {
$entry = '<tr><td>'.htmlspecialchars(trim($opensim->getUserName($row['IdentityID']))).'</td><td><form action="index.php?page=identities" method="post">'.$csrf.'<input type="hidden" name="uuid" value="'.htmlspecialchars($row['IdentityID']).'"><button type="submit" name="enableIdent" class="btn btn-success btn-sm">Aktivieren</button> <button type="submit" name="deleteIdent" class="btn btn-danger btn-sm">Löschen</button></form></td></tr>';
$entry = '<tr><td>'.htmlspecialchars(trim($opensim->getUserName($row['IdentityID']))).'</td><td data-uuid="'.htmlspecialchars($row['IdentityID']).'"><button name="enableIdent" class="btn btn-success btn-sm" data-toggle="modal" data-target="#isc">Aktivieren</button> <button type="submit" name="deleteIdent" class="btn btn-danger btn-sm" data-toggle="modal" data-target="#idc">Löschen</button></td></tr>';
}
$table = $table.$entry;
@ -50,6 +49,7 @@ class Identities extends \Mcp\RequestHandler
$this->app->template('identities.php')->parent('__dashboard.php')->vars([
'title' => 'Identitäten',
'username' => $_SESSION['DISPLAYNAME'],
'activeIdent' => $_SESSION['FIRSTNAME'].' '.$_SESSION['LASTNAME'],
'message' => $message
])->unsafeVar('ident-list', $table.'</tbody></table>')->render();
}
@ -121,14 +121,17 @@ class Identities extends \Mcp\RequestHandler
$statementAccounts = $this->app->db()->prepare('INSERT INTO UserAccounts (PrincipalID, ScopeID, FirstName, LastName, Email, ServiceURLs, Created, UserLevel, UserFlags, UserTitle, active) VALUES (:PrincipalID, :ScopeID, :FirstName, :LastName, :Email, :ServiceURLs, :Created, :UserLevel, :UserFlags, :UserTitle, :active )');
$statementAccounts->execute(['PrincipalID' => $avatarUUID, 'ScopeID' => "00000000-0000-0000-0000-000000000000", 'FirstName' => $avatarNameParts[0], 'LastName' => $avatarNameParts[1], 'Email' => $_SESSION['EMAIL'], 'ServiceURLs' => "HomeURI= GatekeeperURI= InventoryServerURI= AssetServerURI= ", 'Created' => time(), 'UserLevel' => 0, 'UserFlags' => 0, 'UserTitle' => "", 'active' => 1]);
$statementProfile = $this->app->db()->prepare('INSERT INTO `userprofile` (`useruuid`, `profilePartner`, `profileImage`, `profileURL`, `profileFirstImage`, `profileAllowPublish`, `profileMaturePublish`, `profileWantToMask`, `profileWantToText`, `profileSkillsMask`, `profileSkillsText`, `profileLanguages`, `profileAboutText`, `profileFirstText`) VALUES (:useruuid, :profilePartner, :profileImage, :profileURL, :profileFirstImage, :profileAllowPublish, :profileMaturePublish, :profileWantToMask, :profileWantToText, :profileSkillsMask, :profileSkillsText, :profileLanguages, :profileAboutText, :profileFirstText)');
$statementProfile->execute(['useruuid' => $avatarUUID, 'profilePartner' => "00000000-0000-0000-0000-000000000000", 'profileImage' => "00000000-0000-0000-0000-000000000000", 'profileURL' => '', 'profileFirstImage' => "00000000-0000-0000-0000-000000000000", "profileAllowPublish" => "0", "profileMaturePublish" => "0", "profileWantToMask" => "0", "profileWantToText" => "", "profileSkillsMask" => "0", "profileSkillsText" => "", "profileLanguages" => "", "profileAboutText" => "", "profileFirstText" => ""]);
$statementUserIdentitys = $this->app->db()->prepare('INSERT INTO mcp_user_identities (PrincipalID, IdentityID) VALUES (:PrincipalID, :IdentityID)');
$statementUserIdentitys->execute(['PrincipalID' => $_SESSION['UUID'], 'IdentityID' => $avatarUUID]);
} else {
$_SESSION['identities_err'] = 'Dieser Name ist schon in Benutzung.';
}
} else {
$_SESSION['identities_err'] = 'Der Name muss aus einem Vor und einem Nachnamen bestehen.';
$_SESSION['identities_err'] = 'Der Name muss aus einem Vor- und einem Nachnamen bestehen.';
}
}
}

View File

@ -18,28 +18,30 @@ class Profile extends \Mcp\RequestHandler
{
$tpl = $this->app->template('profile.php')->parent('__dashboard.php');
$statement = $this->app->db()->prepare("CREATE TABLE IF NOT EXISTS `iarstates` (`userID` VARCHAR(36) NOT NULL COLLATE 'utf8_unicode_ci', `filesize` BIGINT(20) NOT NULL DEFAULT '0', `iarfilename` VARCHAR(64) NOT NULL COLLATE 'utf8_unicode_ci', `running` INT(1) NOT NULL DEFAULT '0', PRIMARY KEY (`userID`) USING BTREE) COLLATE='utf8_unicode_ci' ENGINE=InnoDB;");
$statement->execute();
//Prüfe ob IAR grade erstellt wird.
$statementIARCheck = $this->app->db()->prepare('SELECT 1 FROM iarstates WHERE userID =:userID');
$statementIARCheck->execute(['userID' => $_SESSION['UUID']]);
$iarRunning = $statementIARCheck->rowCount() != 0;
$statementIARCheck->closeCursor();
if ($iarRunning) {
if (isset($_SESSION['iar_created'])) {
$tpl->unsafeVar('iar-message', '<div class="alert alert-success" role="alert">Deine IAR wird jetzt erstellt und der Download Link wird dir per PM zugesendet.</div>');
} else {
$tpl->unsafeVar('iar-message', '<div class="alert alert-danger" role="alert">Aktuell wird eine IAR erstellt.<br>Warte bitte bis du eine PM bekommst.</div>');
$iarRunning = false;
if (isset($_SESSION['iar_created'])) {
$tpl->unsafeVar('iar-message', '<div class="alert alert-success" role="alert">Deine IAR wird jetzt erstellt und der Download Link wird dir per PM zugesendet.</div>');
unset($_SESSION['iar_created']);
$iarRunning = true;
} else {
$statementIARCheck = $this->app->db()->prepare('SELECT iarfilename,state,created FROM mcp_iar_state WHERE userID =:userID');
$statementIARCheck->execute(['userID' => $_SESSION['UUID']]);
if ($row = $statementIARCheck->fetch()) {
if ($row['state'] < 2) {
$tpl->unsafeVar('iar-message', '<div class="alert alert-danger" role="alert">Aktuell wird eine IAR erstellt.<br>Warte bitte bis du eine PM bekommst.</div>');
$iarRunning = true;
}
else {
$tpl->unsafeVar('iar-message', '<div class="alert alert-success role="alert">Du kannst dir deine IAR (erstellt am '.date('d.m.Y', $row['created']).') <a href="https://'.$this->app->config('domain').'/index.php?api=downloadIar&id='.substr($row['iarfilename'], 0, strlen($row['iarfilename']) - 4).'">hier</a> herunterladen. Sie ist mit dem Passwort <i>password</i> geschützt.</div>');
}
}
$tpl->var('iar-button-state', 'disabled');
$statementIARCheck->closeCursor();
}
else {
$tpl->vars([
'iar-message' => ' ',
'iar-state' => ''
]);
if ($iarRunning) {
$tpl->var('iar-button-state', 'disabled');
}
$opensim = new OpenSim($this->app->db());
@ -75,12 +77,27 @@ class Profile extends \Mcp\RequestHandler
if (isset($_POST['createIAR'])) {
$validator = new FormValidator(array()); // CSRF validation only
if($validator->isValid($_POST)) {
$iarname = md5(time().$_SESSION['UUID'] . rand()).".iar";
$statementIARSTART = $this->app->db()->prepare('INSERT INTO iarstates (userID, filesize, iarfilename) VALUES (:userID, :filesize, :iarfilename)');
$statementIARSTART->execute(['userID' => $_SESSION['UUID'], 'filesize' => 0, 'iarfilename' => $iarname]);
$validRequest = true;
$_SESSION['iar_created'] = true;
$statementIarFile = $this->app->db()->prepare('SELECT iarfilename,state,created FROM mcp_iar_state WHERE userID = ?');
$statementIarFile->execute([$_SESSION['UUID']]);
if ($row = $statementIarFile->fetch()) {
if ($row['state'] == 2) {
unlink($this->app->getDataDir().DIRECTORY_SEPARATOR.'iars'.DIRECTORY_SEPARATOR.$row['iarfilename']);
}
else {
$validRequest = false;
}
}
if ($validRequest) {
$iarname = md5(time().$_SESSION['UUID'] . rand()).".iar";
$statementIARSTART = $this->app->db()->prepare('INSERT INTO mcp_iar_state (userID, filesize, iarfilename) VALUES (:userID, :filesize, :iarfilename) ON DUPLICATE KEY UPDATE filesize = :replFilesize, state = :replState');
$statementIARSTART->execute(['userID' => $_SESSION['UUID'], 'filesize' => 0, 'iarfilename' => $iarname, 'replFilesize' => 0, 'replState' => 0]);
$_SESSION['iar_created'] = true;
}
}
}
elseif (isset($_POST['saveProfileData'])) {

View File

@ -11,7 +11,7 @@ use Mcp\Util\Util;
class ResetPassword extends \Mcp\RequestHandler
{
private const MESSAGE = 'Hallo %%NAME%%,<br/><br/>das Passwort für deinen 4Creative-Account wurde soeben über die Funktion "Passwort vergessen" geändert.<br/><br/>Solltest du diese Änderung nicht selbst durchgeführt haben, wende dich bitte umgehend per E-Mail (info@4creative.net) oder Discord (@ikeytan) an uns.';
private const MESSAGE = 'Hallo %%NAME%%,<br/><br/>das Passwort für deinen 4Creative-Account wurde soeben über die Funktion "Passwort vergessen" geändert.<br/><br/>Solltest du diese Änderung nicht selbst durchgeführt haben, wende dich bitte umgehend per E-Mail (info@4creative.net) oder über unseren Discord-Server an uns.';
private const TOKEN_INVALID = 'Dieser Link zur Passwortzurücksetzung ist nicht gültig. Bitte klicke oder kopiere den Link aus der E-Mail, die du erhalten hast.';
private const TOKEN_EXPIRED = 'Dein Link zur Passwortzurücksetzung ist abgelaufen. Klicke <a href="index.php?page=forgot">hier</a>, um eine neue Anfrage zu senden.';

View File

@ -16,6 +16,7 @@ class SmtpClient
{
$mailer = new PHPMailer(true);
$mailer->isSMTP();
$mailer->CharSet = 'UTF-8';
$mailer->Host = $host;
$mailer->Port = $port;
$mailer->Username = $username;
@ -39,7 +40,7 @@ class SmtpClient
$this->mailer->Subject = $subject;
ob_start();
$tpl->render();
$tplOut = ob_end_clean();
$tplOut = ob_get_flush();
$this->mailer->Body = $tplOut;
$this->mailer->AltBody = $this::htmlToPlain($tplOut);

View File

@ -42,17 +42,20 @@ class Util
return $res;
}
public static function getDataFromHTTP($url, $content = "", $requestTyp = "application/text")
public static function getDataFromHTTP($url, $content = "", $requestType = "application/text")
{
try {
if ($content != "") {
return file_get_contents($url, true, stream_context_create(array('http' => array('header' => 'Content-type: '.$requestTyp, 'method' => 'POST', 'timeout' => 0.5, 'content' => $content))));
} else {
return file_get_contents($url);
}
} catch (Exception $e) {
echo "(HTTP REQUEST) error while conntect to remote server. : ".$url;
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 1);
if ($content != "") {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
curl_setopt($curl, CURLOPT_USERAGENT, 'mcp/0.0.1');
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type' => $requestType
]);
}
return curl_exec($curl);
}
public static function sendInworldIM($fromUUID, $toUUID, $fromName, $targetURL, $text)

View File

@ -35,4 +35,14 @@ password = secret
[grid]
name = OpenSim
main-news = Yet another OpenSim Grid.
homeurl = http://...:8002
homeurl = http://...:8002
; Benötigt für die Anforderung von IARs
[iarfetcher]
; Zugangsdaten der REST-Konsole von OpenSimulator
host = example.com
port = 9001 ; Einstellung console_port
user = mcp
password = secret
; IAR-Verzeichnis aus Sicht der OpenSimulator-Instanz
os-iar-path = /opt/opensim/iars/

View File

@ -1,24 +0,0 @@
<?php
$RUNTIME = array();
$RUNTIME['BASEDIR'] = __DIR__;
set_include_path('.:'.$RUNTIME['BASEDIR']);
include_once "config.php";
if(!isset($RUNTIME['CRON_RESTRICTION'])) {
http_response_code(500);
die();
}
if ($RUNTIME['CRON_RESTRICTION'] != 'none' && (!isset($RUNTIME['CRON_KEY']) || !isset($REQUEST['key']) || $_REQUEST['key'] !== $RUNTIME['CRON_KEY'])) {
http_response_code(401);
die();
}
if ($handle = opendir('./cron/')) {
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
include_once "./cron/".$entry;
}
}
closedir($handle);
}

View File

@ -1,58 +0,0 @@
<?php
include_once 'app/OpenSim.php';
$opensim = new OpenSim();
$statement = $RUNTIME['PDO']->prepare("CREATE TABLE IF NOT EXISTS `iarstates` (`userID` VARCHAR(36) NOT NULL COLLATE 'utf8_unicode_ci', `filesize` BIGINT(20) NOT NULL DEFAULT '0', `iarfilename` VARCHAR(64) NOT NULL COLLATE 'utf8_unicode_ci', `running` INT(1) NOT NULL DEFAULT '0', PRIMARY KEY (`userID`) USING BTREE) COLLATE='utf8_unicode_ci' ENGINE=InnoDB;");
$statement->execute();
$statement = $RUNTIME['PDO']->prepare("SELECT userID,iarfilename,filesize FROM iarstates WHERE running = 1 LIMIT 1");
$statement->execute();
if ($row = $statement->fetch()) {
$email = $opensim->getUserMail($row['userID']);
$fullFilePath = "/var/www/html/data/".$row['iarfilename'];
echo "Aktive IAR für ".$opensim->getUserName($row['userID'])." gefunden. File: ".$fullFilePath."\n";
if (file_exists($fullFilePath)) {
$filesize = filesize($fullFilePath);
if ($filesize != $row['filesize']) {
$statementUpdate = $RUNTIME['PDO']->prepare('UPDATE iarstates SET filesize = :filesize WHERE userID = :userID');
$statementUpdate->execute(['filesize' => $filesize, 'userID' => $row['userID']]);
echo "Status der IAR für ".$opensim->getUserName($row['userID']).": Speichert...\n";
} else {
$APIURL = $RUNTIME['SIDOMAN']['URL']."api.php?CONTAINER=".$RUNTIME['SIDOMAN']['CONTAINER']."&KEY=".$RUNTIME['SIDOMAN']['PASSWORD']."&METODE=RESTART";
$APIResult = file_get_contents($APIURL);
echo "Status der IAR für ".$opensim->getUserName($row['userID']).": Sende Mail...\n";
$statementUpdate = $RUNTIME['PDO']->prepare('DELETE FROM iarstates WHERE userID = :userID');
$statementUpdate->execute(['userID' => $row['userID']]);
sendInworldIM("00000000-0000-0000-0000-000000000000", $row['userID'], "Inventory", $RUNTIME['GRID']['HOMEURL'], "Deine IAR ist fertig zum Download: ".$RUNTIME['IAR']['BASEURL'].$row['iarfilename']);
}
} else {
$name = explode(" ", $opensim->getUserName($row['userID']));
$APIURL = $RUNTIME['SIDOMAN']['URL']."api.php?CONTAINER=".$RUNTIME['SIDOMAN']['CONTAINER']."&KEY=".$RUNTIME['SIDOMAN']['PASSWORD']."&METODE=COMMAND&COMMAND=".urlencode("save iar ".$name[0]." ".$name[1]." /* PASSWORD /downloads/".$row['iarfilename']);
$APIResult = file_get_contents($APIURL);
echo "IAR für ".$name[0]." ".$name[1]." wurde gestartet: Status: ".$APIResult."\n";
}
} else {
$statement = $RUNTIME['PDO']->prepare("SELECT userID,iarfilename FROM iarstates WHERE running = 0 LIMIT 1");
$statement->execute();
while ($row = $statement->fetch()) {
$statementUpdate = $RUNTIME['PDO']->prepare('UPDATE iarstates SET running = :running WHERE userID = :userID');
$statementUpdate->execute(['running' => 1, 'userID' => $row['userID']]);
$name = explode(" ", $opensim->getUserName($row['userID']));
$APIURL = $RUNTIME['SIDOMAN']['URL']."api.php?CONTAINER=".$RUNTIME['SIDOMAN']['CONTAINER']."&KEY=".$RUNTIME['SIDOMAN']['PASSWORD']."&METODE=COMMAND&COMMAND=".urlencode("save iar ".$name[0]." ".$name[1]." /* PASSWORD /downloads/".$row['iarfilename']);
$APIResult = file_get_contents($APIURL);
echo "IAR für ".$name[0]." ".$name[1]." wurde gestartet: Status: ".$APIResult."\n";
}
}

View File

@ -1,36 +0,0 @@
<?php
$statement = $RUNTIME['PDO']->prepare("SELECT id,hash FROM fsassets ORDER BY create_time DESC");
$statement->execute();
$count = 0;
while ($row = $statement->fetch()) {
$fileNameParts = array();
$fileNameParts[0] = substr($row['hash'], 0, 2);
$fileNameParts[1] = substr($row['hash'], 2, 2);
$fileNameParts[2] = substr($row['hash'], 4, 2);
$fileNameParts[3] = substr($row['hash'], 6, 4);
$fileNameParts[4] = $row['hash'].".gz";
//$fileNameParts['Time'] = time();
$fileNameParts['UUID'] = $row['id'];
$fileNameParts['FilePath'] = "/data/assets/base/".$fileNameParts[0]."/".$fileNameParts[1]."/".$fileNameParts[2]."/".$fileNameParts[3]."/".$fileNameParts[4];
if (file_exists($fileNameParts['FilePath'])) {
$filesize = filesize($fileNameParts['FilePath']);
if ($filesize === false) {
continue;
}
}
else {
$filesize = 0;
}
$fileNameParts['FileSize'] = $filesize;
$fileNameParts['Count'] = $count++;
if ($fileNameParts['FileSize'] == 0) {
$add = $RUNTIME['PDO']->prepare('DELETE FROM fsassets WHERE hash = :fileHash');
$add->execute(['fileHash' => $row['hash']]);
}
}

View File

@ -1,19 +0,0 @@
<?php
$InventarCheckStatement = $RUNTIME['PDO']->prepare("UPDATE inventoryitems i SET
i.inventoryName = concat('[DEFEKT] ', i.inventoryName)
WHERE
i.assetID IN (
SELECT
i.assetID
FROM inventoryitems i
WHERE
NOT EXISTS( SELECT *
FROM fsassets fs
WHERE
fs.id = i.assetID
)
AND NOT i.inventoryName LIKE '[DEFEKT] %'
AND i.assetType <> 24
)");
$InventarCheckStatement->execute();

View File

@ -1,102 +0,0 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
include_once 'lib/phpmailer/Exception.php';
include_once 'lib/phpmailer/PHPMailer.php';
include_once 'lib/phpmailer/SMTP.php';
$statement = $RUNTIME['PDO']->prepare("CREATE TABLE IF NOT EXISTS im_offline_send (`id` int(6) NOT NULL DEFAULT 0) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci");
$statement->execute();
function isMailAlreadySent($id)
{
global $RUNTIME;
$statement = $RUNTIME['PDO']->prepare("SELECT 1 FROM im_offline_send WHERE id = ? LIMIT 1");
$statement->execute(array($id));
if ($statement->rowCount() != 0) {
return true;
}
return false;
}
$IMTYP = array(
"0" => "eine Nachricht",
"3" => "eine Gruppeneinladung",
"4" => "ein Inventaritem",
"5" => "eine Bestätigung zur Annahme von Inventar",
"6" => "eine Information zur Ablehnung von Inventar",
"7" => "eine Aufforderung zur Gruppenwahl",
"9" => "ein Inventaritem von einem Script",
"19" => "eine Nachricht von einem Script",
"32" => "eine Gruppennachricht",
"38" => "eine Freundschaftsanfrage",
"39" => "eine Bestätigung über die Annahme der Freundschaft",
"40" => "eine Information über das Ablehnen der Freundschaft"
);
//$statement = $RUNTIME['PDO']->prepare("SELECT * FROM im_offline WHERE PrincipalID = '1148b04d-7a93-49e9-b3c9-ea0cdeec38f7'");
$statement = $RUNTIME['PDO']->prepare("SELECT ID,PrincipalID,Message FROM im_offline");
$statement->execute();
while ($row = $statement->fetch()) {
include_once 'app/OpenSim.php';
$opensim = new OpenSim();
$email = $opensim->getUserMail($row['PrincipalID']);
$allowOfflineIM = $opensim->allowOfflineIM($row['PrincipalID']);
if ($email != "" && $allowOfflineIM == "TRUE") {
if (!isMailAlreadySent($row['ID'])) {
$statementSend = $RUNTIME['PDO']->prepare('INSERT INTO im_offline_send (id) VALUES (:idnummer)');
$statementSend->execute(['idnummer' => $row['ID']]);
$mail = new PHPMailer(true);
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$mail->isSMTP();
$mail->Host = $RUNTIME['SMTP']['SERVER'];
$mail->Port = $RUNTIME['SMTP']['PORT'];
$mail->SMTPAuth = false;
$mail->setFrom($RUNTIME['SMTP']['ADRESS'], $RUNTIME['GRID']['NAME']);
$mail->addAddress($email, $opensim->getUserName($row['PrincipalID']));
$XMLMESSAGE = new SimpleXMLElement($row['Message']);
$HTMLMESSAGE = "Du hast ".$IMTYP["".$XMLMESSAGE->dialog.""]." in ".$RUNTIME['GRID']['NAME']." bekommen. <br><p><ul><li>".htmlspecialchars($XMLMESSAGE->message)."</li></ul></p>Gesendet von: ";
if (isset($XMLMESSAGE->fromAgentName)) {
$HTMLMESSAGE .= $XMLMESSAGE->fromAgentName;
}
if (isset($XMLMESSAGE->RegionID) && isset($XMLMESSAGE->Position)) {
if ($XMLMESSAGE->Position->X != 0 || $XMLMESSAGE->Position->X != 0 || $XMLMESSAGE->Position->X != 0) { //TODO
$HTMLMESSAGE .= " @ ".$opensim->getRegionName($XMLMESSAGE->RegionID)."/".$XMLMESSAGE->Position->X."/".$XMLMESSAGE->Position->Y."/".$XMLMESSAGE->Position->Z;
} else {
$HTMLMESSAGE .= " @ ".$opensim->getRegionName($XMLMESSAGE->RegionID);
}
}
$HTML = new HTML();
$HTML->importHTML("mail.html");
$HTML->setSeitenInhalt($HTMLMESSAGE);
$HTML->build();
$mail->isHTML(true);
$mail->Subject = "Du hast ".$IMTYP["".$XMLMESSAGE->dialog.""]." in ".$RUNTIME['GRID']['NAME'].".";
$mail->Body = $HTML->ausgabe();
$mail->AltBody = strip_tags($HTMLMESSAGE);
//print_r($mail);
$mail->send();
}else{
//echo $row['ID']." wurde bereits gesendet.";
}
}else{
//echo $row['PrincipalID']." möchte keine offline IM oder hat keine E-MAIL Adresse hinterlegt.";
}
}

View File

@ -1,50 +0,0 @@
<?php
$createStatement = $RUNTIME['PDO']->prepare("CREATE TABLE IF NOT EXISTS `regions_info` (`regionID` VARCHAR(36) NOT NULL COLLATE 'utf8_unicode_ci', `RegionVersion` VARCHAR(128) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', `ProcMem` INT(11) NOT NULL, `Prims` INT(11) NOT NULL, `SimFPS` INT(11) NOT NULL, `PhyFPS` INT(11) NOT NULL, `OfflineTimer` INT(11) NOT NULL DEFAULT '0', PRIMARY KEY (`regionID`) USING BTREE) COLLATE='utf8_unicode_ci' ENGINE=InnoDB;");
$createStatement->execute();
$statement = $RUNTIME['PDO']->prepare("SELECT uuid,regionName,owner_uuid,serverURI FROM regions");
$statement->execute();
ini_set('default_socket_timeout', 3);
$ctx = stream_context_create(array('http'=>
array(
'timeout' => 3,
)
));
while($row = $statement->fetch())
{
$result = file_get_contents($row['serverURI']."jsonSimStats", false, $ctx);
if($result == FALSE || $result == "")
{
include 'app/OpenSim.php';
echo "Die Region ".$row['regionName']." von ".$opensim->getUserName($row['owner_uuid'])." ist nicht erreichbar.\n";
$infoStatement = $RUNTIME['PDO']->prepare("SELECT OfflineTimer FROM regions_info WHERE regionID = :regionID");
$infoStatement->execute(['regionID' => $row['uuid']]);
if($infoRow = $infoStatement->fetch())
{
if(($infoRow['OfflineTimer'] + 3600) <= time())
{
echo "Die Region ".$row['regionName']." von ".$opensim->getUserName($row['owner_uuid'])." ist seit über eine Stunde nicht erreichbar!\n";
//sendInworldIM("00000000-0000-0000-0000-000000000000", $row['owner_uuid'], "Region", $RUNTIME['GRID']['HOMEURL'], "WARNUNG: Deine Region '".$row['regionName']."' ist nicht erreichbar und wurde deshalb aus dem Grid entfernt.");
//$statementUpdate = $RUNTIME['PDO']->prepare('DELETE FROM regions WHERE uuid = :uuid');
//$statementUpdate->execute(['uuid' => $row['uuid']]);
}else{
//sendInworldIM("00000000-0000-0000-0000-000000000000", $row['owner_uuid'], "Region", $RUNTIME['GRID']['HOMEURL'], "WARNUNG: Deine Region '".$row['regionName']."' ist nicht erreichbar!");
}
}
}else{
$regionData = json_decode($result);
$statementAccounts = $RUNTIME['PDO']->prepare('REPLACE INTO `regions_info` (`regionID`, `RegionVersion`, `ProcMem`, `Prims`, `SimFPS`, `PhyFPS`, `OfflineTimer`) VALUES (:regionID, :RegionVersion, :ProcMem, :Prims, :SimFPS, :PhyFPS, :OfflineTimer)');
$statementAccounts->execute(['regionID' => $row['uuid'], 'RegionVersion' => $regionData->Version, 'ProcMem' => $regionData->ProcMem, 'Prims' => $regionData->Prims, 'SimFPS' => $regionData->SimFPS, 'PhyFPS' => $regionData->PhyFPS, 'OfflineTimer' => time()]);
}
}
?>

12
public/js/identities.js Normal file
View File

@ -0,0 +1,12 @@
$('#isc').on('show.bs.modal', function(event) {
let identCol = $(event.relatedTarget).parent();
let uuid = identCol.data('uuid');
$('#isc-ident-uuid').attr('value', uuid);
$('#isc-ident-name').text(identCol.prev().text());
});
$('#idc').on('show.bs.modal', function(event) {
let identCol = $(event.relatedTarget).parent();
let uuid = identCol.data('uuid');
$('#idc-ident-uuid').attr('value', uuid);
$('#idc-ident-name').text(identCol.prev().text());
});

View File

@ -139,5 +139,6 @@
<script src="./js/vendor/bootstrap.bundle.min.js"></script>
<script src="./js/vendor/jquery.easing.min.js"></script>
<script src="./js/sb-admin.min.js"></script>
<?= $v['custom-js'] ?>
</body>
</html>

View File

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title><?= $v['title'] ?></title>
<link rel="stylesheet" type="text/css" href="./style/login.min.css">
<link href="./style/4Creative.ico" rel="icon">
<link href="./style/4Creative.ico" rel="apple-touch-icon">
<link href="./img/4Creative.ico" rel="icon">
<link href="./img/4Creative.ico" rel="apple-touch-icon">
</head>
<body>

View File

@ -1,9 +1,7 @@
<div>
Hier kannst du die UUID von deinem Avatar ändern und später jederzeit wieder zurückwechseln. <br>
Inventar und Gruppen bleiben dabei erhalten. <br>
Jede Identität hat ein eigenes Aussehen, ein eigenes Profil und eine eigene Freundesliste.<br>
Nach der Änderung musst du dich neu anmelden.<br>
Jede Identität hat ein eigenes Aussehen, ein eigenes Profil und eine eigene Freundesliste.
</div>
<br><?= $v['message'] ?><br>
@ -16,7 +14,7 @@
<div style="width: 400px; margin: auto; left: 50%;">
Hier kannst du eine neue Identität erstellen.
</div>
<div style="width: 400px; margin: auto; left: 50%;">
<form action="index.php?page=identities" method="post">
<div class="row" style="margin-top: 15px;">
@ -25,7 +23,7 @@
<input type="text" class="form-control" id="newName" name="newName" placeholder="Name">
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col">
<?= $v['csrf'] ?>
@ -36,4 +34,72 @@
</div>
</div>
</div>
<div class="modal fade" id="isc" tabindex="-1" aria-labelledby="iscLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="iscLabel">Identitätswechsel bestätigen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Folgende Daten sind für alle deine Identitäten gleich:
<ul>
<li>Passwort</li>
<li>Inventar</li>
<li>Gruppen</li>
</ul>
Dagegen besitzt du nach dem Wechsel die folgenden, separaten Einstellungen deiner neuen Identität:
<ul>
<li>Name</li>
<li>User-Level</li>
<li>Profil</li>
<li>Freundesliste</li>
</ul>
Möchtest du deine aktive Identität von <b><?= $v['activeIdent'] ?></b> zu <b id="isc-ident-name"></b> wechseln? Du kannst jederzeit zurückwechseln.
</div>
<div class="modal-footer">
<form action="index.php?page=identities" method="post">
<input type="hidden" value="" name="uuid" id="isc-ident-uuid">
<?= $v['csrf'] ?>
<button type="submit" name="enableIdent" class="btn btn-primary btn-success">Identität wechseln</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Abbrechen</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="idc" tabindex="-1" aria-labelledby="idcLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="idcLabel">Löschung der Identität bestätigen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Wenn du eine Identität löschst, werden folgende zu dieser zugehörige Daten gelöscht:
<ul>
<li>Name</li>
<li>User-Level</li>
<li>Profil</li>
<li>Freundesliste</li>
</ul>
Deine anderen Account-Daten sind davon nicht betroffen.<br>
Möchtest du die Identität <b id="idc-ident-name"></b> wirklich löschen?
</div>
<div class="modal-footer">
<form action="index.php?page=identities" method="post">
<input type="hidden" value="" name="uuid" id="idc-ident-uuid">
<?= $v['csrf'] ?>
<button type="submit" name="deleteIdent" class="btn btn-primary btn-danger">Identität löschen</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Abbrechen</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php $v['custom-js'] = '<script src="./js/identities.js"></script>' ?>

View File

@ -29,12 +29,6 @@
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col">
<hr>
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col">
<label for="inputpartner">Partner</label>
@ -42,24 +36,18 @@
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col">
<hr>
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col">
<?= $v['csrf'] ?>
<button type="submit" name="saveProfileData" class="btn btn-primary btn-lg">Speichern</button>
</div>
</div>
</form>
</div>
</div>
<div class="col-md-6">
<div style="width: 400px; margin: auto; left: 50%;">
<div class="row" style="margin-top: 15px;">
<div class="col">
<hr>
</div>
</div>
<form action="index.php?page=profile" method="post">
<div class="row">
<div class="col">
@ -86,17 +74,14 @@
<div class="row" style="margin-top: 15px;">
<div class="col">
<?= $v['csrf'] ?>
<p class="text-center"><button type="submit" name="savePassword" class="btn btn-primary btn-lg">Speichern</button></p>
<button type="submit" name="savePassword" class="btn btn-primary btn-lg">Speichern</button>
</div>
</div>
</form>
<div class="row" style="margin-top: 15px;">
<div class="col">
<hr>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div style="width: 400px; margin: auto; left: 50%;">
<p class="lead"><b>IAR Sicherung</b></p>
<p class="text-center"><?= $v['iar-message'] ?></p>
Hier kannst du eine IAR deines Inventars erstellen.<br>