Compare commits

..

18 Commits
node ... main

Author SHA1 Message Date
9ecc458ea1 Updated version of PHP CodeSniffer; cleaned up some style issues and code smells
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-14 12:52:29 -04:00
069a246db0 Added PHPCS and PHPMD config; added CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-14 12:18:13 -04:00
a5d8d74332 Added PHPCS and PHPMD config; added CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-14 12:17:47 -04:00
205ad74a51 Added PHPCS and PHPMD config; added CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-10-14 12:15:14 -04:00
6c23f3c806 Added a way to edit a server's properties - right now it only edits the server-port property 2022-10-14 11:38:16 -04:00
2297768b57 Added ability to read from a server's properties file, and added server port numbers to the index display 2022-10-14 10:59:28 -04:00
5c35389ae3 Removed beanstalkd stuff for now as it's not necessary; updated the server creation function to actually download the specified server JAR version 2022-10-08 00:02:29 -04:00
fdb31eeb00 Added some basic task runner functionality using beanstalkd and the pheanstalk library 2022-10-07 19:17:05 -04:00
bb584d5ea0 Refactored routes to be under MVC controllers 2022-10-07 17:47:10 -04:00
54199bf7c8 Added a check to make sure the server doesn't exist before creating it 2022-10-06 21:11:54 -04:00
4007b99833 Added some more server info to the server status API 2022-09-24 22:10:23 -04:00
efde09025c Added some AJAX to dynamically update the server's run status 2022-09-24 21:29:12 -04:00
5699c9cf6a Updated the start/stop server icons; added routes to handle server stop/start 2022-09-24 20:45:37 -04:00
b41b246483 Added a nice table for displaying currently available servers 2022-09-24 17:33:59 -04:00
25b68c4bc6 Added some rudimentary server finding 2022-09-24 16:26:57 -04:00
1fd7bb7a9a Added the ability to create a new server 2022-09-22 23:28:06 -04:00
6af39995f7 Added the hassankhan/config module 2022-09-22 22:17:41 -04:00
8c15b10389 Initial PHP Slim project structure; built a very basic site template 2022-09-22 20:37:29 -04:00
34 changed files with 3010 additions and 1995 deletions

7
.gitignore vendored
View File

@ -1,2 +1,5 @@
# NPM dependencies
node_modules/
# Composer dependencies
vendor/
# PHP CodeSniffer cache
.phpcs-cache

37
.woodpecker.yml Normal file
View File

@ -0,0 +1,37 @@
pipeline:
setup:
image: composer:2.4
commands:
- composer install
- mkdir test
- touch test/results.txt
phpcs:
group: test
image: composer:2.4
commands:
- composer run-script phpcs >> test/results.txt
phpmd:
group: test
image: composer:2.4
commands:
- composer run-script phpmd >> test/results.txt
notify:
image: drillster/drone-email
host: smtp.int.metaunix.net
skip_verify: true
from: drone@ci-v1.int.metaunix.net
attachment: test/results.txt
when:
status: [ failure ]
gitea_release:
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://git.metaunix.net
title: "${CI_COMMIT_TAG}"
when:
event: tag

View File

@ -1,64 +0,0 @@
const { exec } = require("child_process");
const fs = require('fs');
class Server {
constructor(dir) {
this.rootDir = dir;
this.name = dir.split('/').at(-1);
this.pidFilePath = this.rootDir + '/pid.txt';
// read version file
var versionFilePath = this.rootDir + '/current_version.txt';
if (fs.existsSync(versionFilePath)) {
this.version = fs.readFileSync(versionFilePath, {encoding:'utf8', flag:'r'});
}
}
start() {
console.log(`Starting server ${this.name}...`);
// create shell command and execute it
let cmd = `${this.rootDir}/start.sh`;
exec(cmd, (err, stdout, stderr) => {
if (err) {
console.log(`Error while starting server: ${error.message}`);
}
console.log(`Server ${this.name} has been started.`);
});
}
stop() {
console.log(`Stopping server ${this.name}...`);
}
getStatus() {
if (fs.existsSync(this.pidFilePath)) {
let pid = fs.readFileSync(this.pidFilePath, {encoding: 'utf8', flag: 'r'});
try {
return process.kill(pid, 0);
} catch (err) {
// ESRCH error means the process doesn't exist, so should return false
if (err.code !== 'ESRCH') {
console.error(err);
return err.code === 'EPERM';
} else {
// delete pid.txt so we don't have to worry about checking it again
fs.unlinkSync(this.pidFilePath);
}
}
}
return false
}
getPid() {
if (fs.existsSync(this.pidFilePath)) {
return fs.readFileSync(this.pidFilePath, {encoding:'utf8', flag:'r'})
}
return -1;
}
}
exports.Server = Server;

4
bin/run-php.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
# start a local instance of the app using PHP's built-in webserver
php -S localhost:8080 -t public/ public/index.php

33
composer.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "bitgoblin/mcst",
"description": "Minecraft Java Edition server management tool",
"type": "project",
"license": "BSD-2-Clause",
"autoload": {
"psr-4": {
"BitGoblin\\MCST\\": "src/"
}
},
"authors": [
{
"name": "Gregory Ballantine",
"email": "gballantine@bitgoblin.tech"
}
],
"require": {
"slim/slim": "^4.10",
"slim/psr7": "^1.5",
"slim/twig-view": "^3.3",
"hassankhan/config": "^3.0",
"php-di/php-di": "^6.4",
"imangazaliev/didom": "^1.13"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5",
"phpmd/phpmd": "^2.5"
},
"scripts": {
"phpcs": "phpcs --standard=./phpcs.xml",
"phpmd": "phpmd src/ text phpmd.xml"
}
}

2213
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +0,0 @@
const express = require('express');
const app = express();
const port = 3000;
// set template engine to Pug
app.set('view engine', 'pug');
// Using express.urlencoded middleware
app.use(express.urlencoded({
extended: true
}));
// import route handlers
var homeRoutes = require('./routes/home');
var serverRoutes = require('./routes/server');
// define routes
app.get('/', homeRoutes.getIndex);
app.get('/server/create', serverRoutes.getCreate);
app.post('/server/create', serverRoutes.postCreate);
app.get('/server/:serverName/start', serverRoutes.getStart);
app.get('/server/:serverName/stop', serverRoutes.getStop);
// set Express to serve static files (ideally this is only used in development)
app.use(express.static('./static/'));
// start Express.js app
app.listen(port, () => {
console.log(`MCST has started and is listening on port ${port}.`);
});

1706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
{
"name": "mcst",
"version": "0.1.0",
"description": "Minecraft Java edition server management tool",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "gitea@git.metaunix.net:BitGoblin/mcst.git"
},
"keywords": [
"minecraft",
"minecraft java edition",
"java"
],
"author": "Gregory Ballantine <gballantine@bitgoblin.tech>",
"license": "BSD-2-Clause",
"dependencies": {
"config": "^3.3.8",
"express": "^4.18.1",
"pug": "^3.0.2"
}
}

35
phpcs.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<ruleset name="PHP_CodeSniffer">
<description>PHPCS configuration file.</description>
<arg name="basepath" value="." />
<arg name="extensions" value="php" />
<arg name="colors" />
<arg name="cache" value=".phpcs-cache" />
<arg value="p" />
<arg value="s" />
<!-- Check PHP files in the src/ directory -->
<file>src/</file>
<!-- Our base rule: set to PSR12-->
<rule ref="PSR12">
<exclude name="PSR12.Classes.OpeningBraceSpace.Found" />
<exclude name="PSR2.Classes.ClassDeclaration.OpenBraceNewLine" />
<exclude name="PSR2.Classes.ClassDeclaration.CloseBraceAfterBody" />
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine" />
</rule>
<!-- Some custom rules -->
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie" />
<rule ref="Generic.Classes.OpeningBraceSameLine" />
<!-- Set indent size to 2 -->
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="2" />
</properties>
</rule>
</ruleset>

19
phpmd.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<ruleset name="PHP CMS rule set"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>
Custom rule set for Pigeon Discord notifier project.
</description>
<!-- Import some rule sets -->
<rule ref="rulesets/cleancode.xml" />
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
</ruleset>

1
pid_manual.txt Normal file
View File

@ -0,0 +1 @@
628624

5
public/.htaccess Normal file
View File

@ -0,0 +1,5 @@
# rewrite rules
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

View File

@ -2,6 +2,18 @@ body{
background-color: #e6e6e6;
}
input[type=submit]:disabled,
button:disabled{
background-color: darkgrey;
}
input[type=submit]:disabled:hover,
button:disabled:hover{
background-color: darkgrey;
border: 1px solid #bbb;
color: #555;
cursor: not-allowed;
}
/* set the max-width for centered content */
.container{
max-width: 1100px;

24
public/index.php Normal file
View File

@ -0,0 +1,24 @@
<?php
// if we're looking for static files in dev, return false so they can be served.
if (PHP_SAPI == 'cli-server') {
$url = parse_url($_SERVER['REQUEST_URI']);
$file = __DIR__ . $url['path'];
// check the file types, only serve standard files
if (preg_match('/\.(?:png|js|jpg|jpeg|gif|css)$/', $file)) {
// does the file exist? If so, return it
if (is_file($file))
return false;
// file does not exist. return a 404
header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
printf('"%s" does not exist', $_SERVER['REQUEST_URI']);
return false;
}
}
require_once __DIR__ . '/../src/app.php';
$app->run();

29
public/js/drake.js Normal file
View File

@ -0,0 +1,29 @@
$(document).ready(function () {
// periodically check for server status updates
$('.serverItem').each(function() {
setInterval(updateServer, 5000, $(this));
});
// set the serverName input field to check if the server exists
$('input#serverName').on('change', checkServerExists);
});
function updateServer(elem) {
var serverName = elem.data('server-name');
$.get('/server/' + serverName + '/status', function(data, state) {
elem.children('.serverState').eq(0).text(data.state);
});
}
function checkServerExists() {
$.get('/server/' + $(this).val() + '/status', function(data, state) {
if (data.exists) {
$('input#createSubmit').prop('disabled', true);
alert('That server name is already used; please use another name!');
} else {
if ($('input#createSubmit').prop('disabled')) {
$('input#createSubmit').prop('disabled', false);
}
}
});
}

View File

@ -1,19 +0,0 @@
const config = require('config');
const fs = require('fs');
const path = require('path');
const minecraft = require('../app/MinecraftServer');
exports.getIndex = function(req, res) {
// search for minecraft server directories
let rootDir = config.get('server_directory');
let serverDirs = fs.readdirSync(rootDir);
let servers = [];
for (let i = 0; i < serverDirs.length; i++) {
servers.push(new minecraft.Server(path.join(rootDir, serverDirs[i])));
}
// render view
res.render('index', {
servers: servers,
});
};

View File

@ -1,70 +0,0 @@
const config = require('config');
const fs = require('fs');
const https = require('https');
const path = require('path');
const minecraft = require('../app/MinecraftServer');
exports.getCreate = function(req, res) {
// render view
res.render('create');
};
exports.postCreate = function(req, res) {
mcDir = config.get('server_directory');
serverDir = path.join(mcDir, req.body.serverName);
// make server directory
fs.mkdirSync(serverDir);
// grab the server JAR file
serverJarUrl = 'https://piston-data.mojang.com/v1/objects/f69c284232d7c7580bd89a5a4931c3581eae1378/server.jar';
serverJarName = "server_" + req.body.serverVersion + ".jar";
serverJarPath = path.join(serverDir, serverJarName);
https.get(serverJarUrl, (res) => {
const filePath = fs.createWriteStream(serverJarPath);
res.pipe(filePath);
filePath.on('finish', () => {
filePath.close();
console.log('Download Completed');
});
});
// create the start.sh shell script
scriptFilePath = path.join(serverDir, 'start.sh');
scriptContent = "#!/bin/sh\n\ncd " + serverDir + "\njava -Xmx2048M -Xms2048M -jar server_" + req.body.serverVersion + ".jar nogui &\n\necho \"$!\" > ./pid.txt";
fs.writeFileSync(scriptFilePath, scriptContent);
fs.chmodSync(scriptFilePath, 0o755);
// save the current version to a text file - this will hopefully be temporary solution
versionFilePath = path.join(serverDir, 'current_version.txt');
versionContent = req.body.serverVersion;
fs.writeFileSync(versionFilePath, versionContent);
fs.chmodSync(versionFilePath, 0o644);
// redirect the user back to the home page
res.redirect('/');
};
exports.getStart = function(req, res) {
let serverName = req.params.serverName;
let rootDir = config.get('server_directory');
let server = new minecraft.Server(path.join(rootDir, serverName));
// start the server
server.start();
// redirect the user back to the home page
res.redirect('/');
};
exports.getStop = function(req, res) {
let serverName = req.params.serverName;
let rootDir = config.get('server_directory');
let server = new minecraft.Server(path.join(rootDir, serverName));
// stop the server
server.stop();
// redirect the user back to the home page
res.redirect('/');
};

View File

@ -0,0 +1,19 @@
<?php
namespace BitGoblin\MCST\Controllers;
use Psr\Container\ContainerInterface;
class Controller {
protected $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function get(string $name) {
return $this->container->get($name);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace BitGoblin\MCST\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use BitGoblin\MCST\Minecraft\Server;
class HomeController extends Controller {
public function getIndex(Request $request, Response $response): Response {
$config = $this->get('config');
// find servers
$minecraftDir = $config->get('server_directory');
$serverDirs = glob($minecraftDir . "/*", GLOB_ONLYDIR);
$minecraftServers = array();
foreach ($serverDirs as $m) {
array_push($minecraftServers, new Server($m));
}
$view = Twig::fromRequest($request);
return $view->render($response, 'index.twig', [
'servers' => $minecraftServers,
]);
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace BitGoblin\MCST\Controllers;
use DiDom\Document;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use BitGoblin\MCST\Minecraft\Server;
class ServerController extends Controller {
// GET create server route
public function getCreate(Request $request, Response $response): Response {
$view = Twig::fromRequest($request);
return $view->render($response, 'create.twig');
}
// POST create server route
public function postCreate(Request $request, Response $response): Response {
// set up the POST parameters and grab our config
$params = (array)$request->getParsedBody();
$config = $this->get('config');
// create our server directory
$serverDir = join('/', array($config->get('server_directory'), $params['serverName']));
mkdir($serverDir);
// find the server JAR URL for the version
$versionPageLink = 'https://minecraft.fandom.com/wiki/Java_Edition_' . $params['serverVersion'];
$dom = new Document($versionPageLink, true);
$infobox = $dom->find('.notaninfobox')[0];
$serverLinkElem = $infobox->find("a:contains('Server')");
$serverJarUrl = $serverLinkElem[0]->attr('href');
// grab the server JAR file
$serverJarName = "server_" . $params['serverVersion'] . ".jar";
$serverJarPath = join('/', array($serverDir, $serverJarName));
file_put_contents($serverJarPath, file_get_contents($serverJarUrl));
// create the start.sh shell script
$scriptFilePath = join('/', array($serverDir, 'start.sh'));
$scriptFile = fopen($scriptFilePath, 'w');
$scriptContent = "#!/bin/sh\n\ncd " . $serverDir . "\njava -Xmx2048M -Xms2048M -jar server_" . $params['serverVersion'] . ".jar nogui";
fwrite($scriptFile, $scriptContent);
fclose($scriptFile);
chmod($scriptFilePath, 0755);
// save the current version to a text file - this will hopefully be temporary solution
$versionFilePath = join('/', array($serverDir, 'current_version.txt'));
$versionFile = fopen($versionFilePath, 'w');
$versionContent = $params['serverVersion'];
fwrite($versionFile, $versionContent);
fclose($versionFile);
chmod($versionFilePath, 0644);
// redirect the user back to the home page
return $response
->withHeader('Location', '/')
->withStatus(302);
}
// GET server status route
public function getStatus(Request $request, Response $response, $args): Response {
$config = $this->get('config');
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
// check if the server exists - if not, return a false result
if (!is_dir($serverDir)) {
$response->getBody()->write(json_encode(array('exists' => false)));
return $response
->withHeader('Content-Type', 'application/json');
}
// create server object and pass info back to client as JSON data
$server = new Server($serverDir);
$serverData = [
'exists' => true,
'name' => $server->getName(),
'version' => $server->getVersion(),
'state' => $server->getState() ? 'Running' : 'Stopped',
];
$response->getBody()->write(json_encode($serverData));
return $response
->withHeader('Content-Type', 'application/json');
}
// GET server start route
public function getStart(Request $request, Response $response, $args): Response {
$config = $this->get('config');
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
// create server object and start it
$server = new Server($serverDir);
$server->start();
// redirect the user back to the home page
return $response
->withHeader('Location', '/')
->withStatus(302);
}
// GET server stop route
public function getStop(Request $request, Response $response, $args): Response {
$config = $this->get('config');
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
// create server object and start it
$server = new Server($serverDir);
$server->stop();
// redirect the user back to the home page
return $response
->withHeader('Location', '/')
->withStatus(302);
}
// GET edit server properties route
public function getEdit(Request $request, Response $response, $args): Response {
$config = $this->get('config');
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
$server = new Server($serverDir);
$data = array(
'serverName' => $args['serverName'],
'serverPort' => $server->getProperty('server-port'),
);
$view = Twig::fromRequest($request);
return $view->render($response, 'edit.twig', $data);
}
// POST edit server properties route
public function postEdit(Request $request, Response $response, $args): Response {
// set up the POST parameters and grab our config
$params = (array)$request->getParsedBody();
$config = $this->get('config');
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
// create server object and save new value
$server = new Server($serverDir);
$server->updateProperty('server-port', $params['serverPort']);
// redirect the user back to the home page
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}

111
src/Minecraft/Server.php Normal file
View File

@ -0,0 +1,111 @@
<?php
namespace BitGoblin\MCST\Minecraft;
class Server {
// server root directory
private string $rootDir;
// server name
private string $serverName;
// server Minecraft version
private string $serverVersion;
// PID file path
private string $pidFilePath;
// server run status
private bool $state = false;
// server config properties
private ServerConfig $properties;
// class constructor
public function __construct($dir) {
$dirBits = explode('/', $dir);
$this->rootDir = $dir;
$this->serverName = $dirBits[count($dirBits) - 1];
$this->pidFilePath = $this->rootDir . '/pid.txt';
$this->properties = new ServerConfig($this->rootDir . '/server.properties');
// get server version
$versionFile = join('/', array($this->rootDir, 'current_version.txt'));
if (file_exists($versionFile)) {
$this->serverVersion = file($versionFile)[0];
}
// search for server PID file
if (file_exists($this->pidFilePath)) {
// get and search for PID
$pid = trim($this->getPid());
if (file_exists('/proc/' . $pid)) {
// set server state to true since it is running
$this->state = true;
} else {
// delete the PID file since it's no longer needed
unlink($this->pidFilePath);
}
}
}
// start the server
public function start(): void {
$command = $this->rootDir . '/start.sh > /dev/null 2>&1 & echo $!';
exec($command, $output);
// use lsof to find the PID of the server
$pidResult = exec('lsof ' . $this->rootDir . '/server*.jar | grep .jar');
$pid = preg_split('/\s+/', $pidResult, -1, PREG_SPLIT_NO_EMPTY)[1];
$pidFile = fopen($this->pidFilePath, 'w');
fwrite($pidFile, $pid);
fclose($pidFile);
// set server state to true
$this->state = true;
}
// stop the server
public function stop(): void {
$pid = $this->getPid();
if ($pid != -1) {
// kill the process gracefully
exec('kill -15 ' . $pid);
// set server state to false
$this->state = false;
}
}
// get the server's PID
private function getPid(): int {
// search for server PID file
if (file_exists($this->pidFilePath)) {
// get and return server PID
return trim(file($this->pidFilePath)[0]);
}
// return a 'false' result
return -1;
}
// get property from server properties file
public function getProperty(string $param) {
return $this->properties->get($param);
}
// update property in server properties file
public function updateProperty(string $paramName, $paramValue) {
$this->properties->update($paramName, $paramValue);
}
// getters & setters
public function getDirectory(): string {
return $this->rootDir;
}
public function getName(): string {
return $this->serverName;
}
public function getVersion(): string {
return $this->serverVersion;
}
public function getState(): bool {
return $this->state;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace BitGoblin\MCST\Minecraft;
class ServerConfig {
// path to the server.properties file
protected string $propertiesFile;
// loaded properties
protected array $config;
public function __construct($propertiesFile) {
$this->propertiesFile = $propertiesFile; // save the properties file
$this->config = array(); // initialize array
// parse config file
$fileHandle = fopen($propertiesFile, 'r+');
while (!feof($fileHandle)) {
$line = fgets($fileHandle);
$eqpos = strpos($line, '=');
$parameterName = substr($line, 0, $eqpos);
$parameterValue = trim(substr($line, ($eqpos + 1)));
// assign parameter values to the config array
$this->config[$parameterName] = $parameterValue;
}
}
public function get(string $param) {
return $this->config[$param];
}
public function update(string $paramName, $paramValue) {
// update the local variable value
$this->config[$paramName] = $paramValue;
// update and save the server.properties file
$newFileContent = '';
$fileHandle = fopen($this->propertiesFile, 'r+');
while (!feof($fileHandle)) {
$line = fgets($fileHandle);
if (!str_starts_with($line, '#')) {
$eqpos = strpos($line, '=');
$parameterName = substr($line, 0, $eqpos);
$parameterValue = substr($line, ($eqpos + 1));
// use the new value if it's being updated
if ($parameterName == $paramName) {
$parameterValue = $paramValue . "\n";
}
$newFileContent .= $parameterName . '=' . $parameterValue;
} else {
$newFileContent .= $line;
}
}
// save the new properties file
file_put_contents($this->propertiesFile, $newFileContent);
fclose($fileHandle);
}
}

35
src/app.php Normal file
View File

@ -0,0 +1,35 @@
<?php
use DI\Container;
use Noodlehaus\Config;
use Noodlehaus\Parser\Json;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
require __DIR__ . '/../vendor/autoload.php';
// Load app configuration
$config = Config::load(__DIR__ . '/../conf/defaults.json');
// Create new container object and add our config object to it
$container = new Container();
$container->set('config', function () use ($config) {
return $config;
});
// Set container to create App with on AppFactory
AppFactory::setContainer($container);
$app = AppFactory::create();
// Add Error Handling Middleware
$app->addErrorMiddleware(true, false, false);
// Create Twig
$twig = Twig::create(__DIR__ . '/../views', ['cache' => false]);
// Add Twig-View Middleware
$app->add(TwigMiddleware::create($app, $twig));
// load in route handlers
require_once __DIR__ . '/routes.php';

27
src/routes.php Normal file
View File

@ -0,0 +1,27 @@
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use BitGoblin\MCST\Minecraft\Server;
// index GET route - this page should welcome the user and direct them to the available actions
$app->get('/', '\\BitGoblin\\MCST\\Controllers\\HomeController:getIndex')->setName('index');
// create GET route - this page allows a user to create a new server instance
$app->get('/create', '\\BitGoblin\\MCST\\Controllers\\ServerController:getCreate')->setName('create');
// create POST route - processes the new server creation
$app->post('/create', '\\BitGoblin\\MCST\\Controllers\\ServerController:postCreate');
// server status route
$app->get('/server/{serverName}/status', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStatus')->setName('server.status');
// server start route
$app->get('/server/{serverName}/start', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStart')->setName('server.start');
// server stop route
$app->get('/server/{serverName}/stop', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStop')->setName('server.stop');
// edit GET route - this page allows a user to edit a server's properties
$app->get('/server/{serverName}/edit', '\\BitGoblin\\MCST\\Controllers\\ServerController:getEdit')->setName('edit');
// edit POST route - processes the edits a server's properties file
$app->post('/server/{serverName}/edit', '\\BitGoblin\\MCST\\Controllers\\ServerController:postEdit');

View File

@ -1,3 +0,0 @@
$(document).ready(function () {
// This will be used when needed
});

View File

@ -1,24 +0,0 @@
extends layout.pug
block content
header.row
div.columns.twelve
h1 Create new server
section.row
div.columns.twelve
form(action='/server/create', method='POST')
div.row
div.columns.six
label(for='serverName') Server name:
input#serverName.u-full-width(name='serverName', type='text', placeholder='MyServer')
div.columns.six
label(for='serverVersion') Minecraft version:
input#serverVersion.u-full-width(name='serverVersion', type='text', placeholder='1.19.2')
input(type='submit', value='Submit')
div.row
div.columns.twelve
p
a(href='/') Back

40
views/create.twig Normal file
View File

@ -0,0 +1,40 @@
{% extends 'layout.twig' %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Create new server</h1>
</div>
</header>
<!-- create server form -->
<section class="row">
<div class="columns twelve">
<form action="/create" method="POST">
<div class="row">
<div class="columns six">
<label for="serverName">Server name:</label>
<input id="serverName" name="serverName" class="u-full-width" type="text" placeholder="MyServer" required>
</div>
<div class="columns six">
<label for="serverVersion">Minecraft version:</label>
<input id="serverVersion" name="serverVersion" class="u-full-width" type="text" placeholder="1.19.2" required>
</div>
</div>
<input id="createSubmit" type="submit" value="Submit">
</form>
</div>
</section>
<!-- lower navigation -->
<div class="row">
<div class="columns twelve">
<p><a href="/">Back</a></p>
</div>
</div>
{% endblock %}

35
views/edit.twig Normal file
View File

@ -0,0 +1,35 @@
{% extends 'layout.twig' %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Edit server: {{ serverName }}</h1>
</div>
</header>
<!-- create server form -->
<section class="row">
<div class="columns twelve">
<form action="/server/{{ serverName }}/edit" method="POST">
<div class="row">
<div class="columns twelve">
<label for="serverPort">Server port:</label>
<input id="serverPort" name="serverPort" class="u-full-width" type="number" placeholder="25565" required value={{ serverPort }}>
</div>
</div>
<input id="editSubmit" type="submit" value="Submit">
</form>
</div>
</section>
<!-- lower navigation -->
<div class="row">
<div class="columns twelve">
<p><a href="/">Back</a></p>
</div>
</div>
{% endblock %}

View File

@ -1,30 +0,0 @@
extends layout.pug
block content
header.row
div.columns.twelve
h1 Welcome to MCST!
p Using MCST you can easily manage your Minecraft: Java Edition servers.
section.row
div.columns.twelve
h3 List of servers:
if servers.length < 1
p There are currently no servers registered.
else
table.u-full-width
thead
tr
th Server name
th Minecraft version
th State
th Actions
tbody
each m in servers
tr.serverItem
td.serverName= m.name
td.serverVersion= m.version
td.serverState= m.getStatus()
td
a(href='/server/' + m.name + '/start') Start
a(href='/server/' + m.name + '/stop') Stop

50
views/index.twig Normal file
View File

@ -0,0 +1,50 @@
{% extends 'layout.twig' %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Welcome to MCST!</h1>
<p>Using MCST you can easily manage your Minecraft: Java Edition servers.</p>
</div>
</header>
<!-- list of servers -->
<section class="row">
<div class="columns twelve">
<h3>List of servers:</h3>
{% if servers|length < 1 %}
<p>There are currently no servers registered.</p>
{% else %}
<table class="u-full-width">
<thead>
<tr>
<th>Server name</th>
<th>Minecraft version</th>
<th>Port</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for m in servers %}
<tr class="serverItem" data-server-name="{{ m.getName() }}">
<td class="serverName">{{ m.getName() }}</td>
<td class="serverVersion">{{ m.getVersion() }}</td>
<td class="serverPort">{{ m.getProperty('server-port') }}</td>
<td class="serverState">{{ m.getState() ? 'Running' : 'Stopped' }}</td>
<td>
<a href="/server/{{ m.getName() }}/start"><i class="fa-solid fa-play"></i></a>
<a href="/server/{{ m.getName() }}/stop"><i class="fa-solid fa-stop"></i></a>
<a href="/server/{{ m.getName() }}/edit"><i class="fa-solid fa-pen-to-square"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -1,21 +0,0 @@
doctype html
html(lang="en")
head
title= pageTitle
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css')
link(rel='stylesheet', href='/css/wyrm.css')
script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js')
script(src='/js/drake.js')
body
// global navigation
nav.navbar
div.navbar-left
ul
li.menu-text MCST
li: a(href='/') Home
li: a(href='/server/create') Create
li: a(href='/status') Status
// main content
div#main-content.container
block content

32
views/layout.twig Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minecraft Server Tool</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<link rel="stylesheet" href="/css/wyrm.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="/js/drake.js"></script>
</head>
<body>
<!-- global navigation -->
<nav class="navbar">
<div class="navbar-left">
<ul>
<li class="menu-text">MCST</li>
<li><a href="/">Home</a></li>
<li><a href="/create">Create</a></li>
<li><a href="/status">Status</a></li>
</ul>
</div>
</nav>
<!-- main content -->
<div id="main-content" class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>