This commit is contained in:
26
src/Api/Action/AnalyseLogAction.php
Normal file
26
src/Api/Action/AnalyseLogAction.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\LogContentParser;
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\CodexLogResponse;
|
||||
use Aternos\Mclogs\Log;
|
||||
|
||||
class AnalyseLogAction extends ApiAction
|
||||
{
|
||||
public function runApi(): ApiResponse
|
||||
{
|
||||
$data = new LogContentParser()->getContent();
|
||||
|
||||
if ($data instanceof ApiError) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$content = $data['content'];
|
||||
$log = new Log()->setContent($content);
|
||||
|
||||
return new CodexLogResponse($log->getCodexLog());
|
||||
}
|
||||
}
|
||||
37
src/Api/Action/ApiAction.php
Normal file
37
src/Api/Action/ApiAction.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\ContentParser;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
abstract class ApiAction extends Action
|
||||
{
|
||||
abstract protected function runApi(): ApiResponse;
|
||||
|
||||
protected function getAllowedOrigin(): string
|
||||
{
|
||||
return '*';
|
||||
}
|
||||
|
||||
protected function shouldAllowCredentials(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function run(): bool
|
||||
{
|
||||
header('Access-Control-Allow-Origin: ' . $this->getAllowedOrigin());
|
||||
header('Access-Control-Allow-Headers: *');
|
||||
if ($this->shouldAllowCredentials()) {
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
header("Accept-Encoding: " . implode(",", ContentParser::getSupportedEncodings()));
|
||||
|
||||
$response = $this->runApi();
|
||||
$response->output();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
77
src/Api/Action/BulkDeleteLogsAction.php
Normal file
77
src/Api/Action/BulkDeleteLogsAction.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\ContentParser;
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\MultiResponse;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Storage\MongoDBClient;
|
||||
|
||||
class BulkDeleteLogsAction extends ApiAction
|
||||
{
|
||||
public const int MAX_IDS = 256;
|
||||
|
||||
/**
|
||||
* @return ApiResponse
|
||||
*/
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
$data = new ContentParser()->getContent();
|
||||
if ($data instanceof ApiError) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (count($data) === 0) {
|
||||
return new ApiError(400, "No logs provided.");
|
||||
}
|
||||
if (count($data) > static::MAX_IDS) {
|
||||
return new ApiError(400, "Too many logs. Maximum is " . static::MAX_IDS . ".");
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($data as $log) {
|
||||
if (!is_array($log)) {
|
||||
return new ApiError(400, "Each entry must be an object with 'id' and 'token' fields.");
|
||||
}
|
||||
if (!isset($log["id"]) || !is_string($log["id"]) ||
|
||||
!preg_match("/^" . Id::PATTERN . "$/", $log["id"])) {
|
||||
return new ApiError(400, "Each log must have a valid 'id' field.");
|
||||
}
|
||||
if (!isset($log["token"]) || !is_string($log["token"])) {
|
||||
return new ApiError(400, "Each log must have a valid 'token' field.");
|
||||
}
|
||||
$ids[] = $log["id"];
|
||||
}
|
||||
|
||||
$logs = Log::findAll($ids, false);
|
||||
|
||||
$deleteIds = [];
|
||||
$response = new MultiResponse();
|
||||
foreach ($data as $log) {
|
||||
$id = $log["id"];
|
||||
$token = $log["token"];
|
||||
|
||||
$log = $logs[$id] ?? null;
|
||||
if (!$log) {
|
||||
$response->addResponse($id, new ApiError(404, "Log not found."));
|
||||
continue;
|
||||
}
|
||||
|
||||
$logToken = $log->getToken();
|
||||
if (!$logToken || !$logToken->matches($token)) {
|
||||
$response->addResponse($id, new ApiError(403, "Invalid token."));
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleteIds[] = $id;
|
||||
$response->addResponse($id, new ApiResponse());
|
||||
}
|
||||
|
||||
MongoDBClient::getInstance()->deleteLogs($deleteIds);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
43
src/Api/Action/CreateLogAction.php
Normal file
43
src/Api/Action/CreateLogAction.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\LogContentParser;
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\LogResponse;
|
||||
use Aternos\Mclogs\Data\MetadataEntry;
|
||||
use Aternos\Mclogs\Log;
|
||||
|
||||
class CreateLogAction extends ApiAction
|
||||
{
|
||||
protected bool $includeCookie = false;
|
||||
protected bool $includeToken = true;
|
||||
|
||||
public function runApi(): ApiResponse
|
||||
{
|
||||
$data = new LogContentParser()->getContent();
|
||||
|
||||
if ($data instanceof ApiError) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$content = $data['content'];
|
||||
$metadata = [];
|
||||
if (isset($data['metadata']) && is_array($data['metadata'])) {
|
||||
$metadata = MetadataEntry::allFromArray($data['metadata']);
|
||||
}
|
||||
$source = null;
|
||||
if (isset($data['source']) && is_string($data['source'])) {
|
||||
$source = $data['source'];
|
||||
}
|
||||
|
||||
$log = Log::create($content, $metadata, $source);
|
||||
|
||||
if ($this->includeCookie) {
|
||||
$log->setTokenCookie();
|
||||
}
|
||||
|
||||
return new LogResponse($log, $this->includeToken);
|
||||
}
|
||||
}
|
||||
60
src/Api/Action/DeleteLogAction.php
Normal file
60
src/Api/Action/DeleteLogAction.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class DeleteLogAction extends ApiAction
|
||||
{
|
||||
protected function getRequestToken(): ?string
|
||||
{
|
||||
$authorizationHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
|
||||
if (!$authorizationHeader) {
|
||||
return null;
|
||||
}
|
||||
$parts = explode(" ", $authorizationHeader);
|
||||
return $parts[1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ApiResponse
|
||||
*/
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
$requestToken = $this->getRequestToken();
|
||||
|
||||
if (!$requestToken) {
|
||||
return new ApiError(400, "Missing token.");
|
||||
}
|
||||
|
||||
$id = new Id(URL::getLastPathPart());
|
||||
$log = Log::find($id);
|
||||
|
||||
if (!$log) {
|
||||
return new ApiError(404, "Log not found.");
|
||||
}
|
||||
|
||||
$token = $log->getToken();
|
||||
if (!$token || !$token->matches($requestToken)) {
|
||||
return new ApiError(403, "Invalid token.");
|
||||
}
|
||||
|
||||
$deleted = $log->delete();
|
||||
if (!$deleted) {
|
||||
return new ApiError(500, "Failed to delete log.");
|
||||
}
|
||||
|
||||
$this->handleDeletedLog($log);
|
||||
|
||||
return new ApiResponse();
|
||||
}
|
||||
|
||||
protected function handleDeletedLog(Log $log): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
13
src/Api/Action/EmptyAction.php
Normal file
13
src/Api/Action/EmptyAction.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
class EmptyAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
14
src/Api/Action/EndpointNotFoundAction.php
Normal file
14
src/Api/Action/EndpointNotFoundAction.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
|
||||
class EndpointNotFoundAction extends ApiAction
|
||||
{
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
return new ApiError(404, "Could not find endpoint.");
|
||||
}
|
||||
}
|
||||
14
src/Api/Action/GetFiltersAction.php
Normal file
14
src/Api/Action/GetFiltersAction.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\FiltersResponse;
|
||||
|
||||
class GetFiltersAction extends ApiAction
|
||||
{
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
return new FiltersResponse();
|
||||
}
|
||||
}
|
||||
14
src/Api/Action/GetLimitsAction.php
Normal file
14
src/Api/Action/GetLimitsAction.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\LimitsResponse;
|
||||
|
||||
class GetLimitsAction extends ApiAction
|
||||
{
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
return new LimitsResponse();
|
||||
}
|
||||
}
|
||||
28
src/Api/Action/LogInfoAction.php
Normal file
28
src/Api/Action/LogInfoAction.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\LogResponse;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class LogInfoAction extends ApiAction
|
||||
{
|
||||
/**
|
||||
* @return ApiResponse
|
||||
*/
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
$id = new Id(URL::getLastPathPart());
|
||||
$log = Log::find($id);
|
||||
|
||||
if (!$log) {
|
||||
return new ApiError(404, "Log not found.");
|
||||
}
|
||||
|
||||
return new LogResponse($log);
|
||||
}
|
||||
}
|
||||
31
src/Api/Action/LogInsightsAction.php
Normal file
31
src/Api/Action/LogInsightsAction.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\CodexLogResponse;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class LogInsightsAction extends ApiAction
|
||||
{
|
||||
/**
|
||||
* @return ApiResponse
|
||||
*/
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
$id = new Id(URL::getLastPathPart());
|
||||
$log = Log::find($id);
|
||||
|
||||
if (!$log) {
|
||||
return new ApiError(404, "Log not found.");
|
||||
}
|
||||
|
||||
$codexLog = $log->getCodexLog();
|
||||
$codexLog->setIncludeEntries(false);
|
||||
|
||||
return new CodexLogResponse($codexLog);
|
||||
}
|
||||
}
|
||||
17
src/Api/Action/RateLimitErrorAction.php
Normal file
17
src/Api/Action/RateLimitErrorAction.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
|
||||
class RateLimitErrorAction extends ApiAction
|
||||
{
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
return new ApiError(
|
||||
429,
|
||||
"Unfortunately you have exceeded the rate limit for the current time period. Please try again later."
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/Api/Action/RawLogAction.php
Normal file
28
src/Api/Action/RawLogAction.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Action;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Api\Response\ApiResponse;
|
||||
use Aternos\Mclogs\Api\Response\RawLogResponse;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class RawLogAction extends ApiAction
|
||||
{
|
||||
/**
|
||||
* @return ApiResponse
|
||||
*/
|
||||
protected function runApi(): ApiResponse
|
||||
{
|
||||
$id = new Id(URL::getLastPathPart());
|
||||
$log = Log::find($id);
|
||||
|
||||
if (!$log) {
|
||||
return new ApiError(404, "Log not found.");
|
||||
}
|
||||
|
||||
return new RawLogResponse($log);
|
||||
}
|
||||
}
|
||||
29
src/Api/ApiRouter.php
Normal file
29
src/Api/ApiRouter.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api;
|
||||
|
||||
use Aternos\Mclogs\Router\Router;
|
||||
use Aternos\Mclogs\Frontend;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Router\Method;
|
||||
|
||||
class ApiRouter extends Router
|
||||
{
|
||||
protected function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->register(Method::GET, "#^/$#", new Frontend\Action\ApiDocsAction())
|
||||
->register(Method::OPTIONS, "#^/.*$#", new Action\EmptyAction())
|
||||
->register(Method::POST, "#^/1/log/?$#", new Action\CreateLogAction())
|
||||
->register(Method::GET, "#^/1/log/" . Id::PATTERN . "$#", new Action\LogInfoAction())
|
||||
->register(Method::DELETE, "#^/1/log/" . Id::PATTERN . "$#", new Action\DeleteLogAction())
|
||||
->register(Method::POST, "#^/1/bulk/log/delete/?$#", new Action\BulkDeleteLogsAction())
|
||||
->register(Method::GET, "#^/1/insights/" . Id::PATTERN . "$#", new Action\LogInsightsAction())
|
||||
->register(Method::GET, "#^/1/raw/" . Id::PATTERN . "$#", new Action\RawLogAction())
|
||||
->register(Method::POST, "#^/1/analyse/?$#", new Action\AnalyseLogAction())
|
||||
->register(Method::GET, "#^/1/errors/rate$#", new Action\RateLimitErrorAction())
|
||||
->register(Method::GET, "#^/1/limits$#", new Action\GetLimitsAction())
|
||||
->register(Method::GET, "#^/1/filters#", new Action\GetFiltersAction())
|
||||
->setDefaultAction(new Action\EndpointNotFoundAction());
|
||||
}
|
||||
}
|
||||
85
src/Api/ContentParser.php
Normal file
85
src/Api/ContentParser.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
/**
|
||||
* Utility class for reading log content from the http request
|
||||
*/
|
||||
class ContentParser
|
||||
{
|
||||
protected const int MAX_ENCODING_STEPS = 5;
|
||||
|
||||
/**
|
||||
* Get all supported content encodings
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getSupportedEncodings(): array
|
||||
{
|
||||
return ["deflate", "gzip", "x-gzip"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content from the http request
|
||||
*
|
||||
* @return array|ApiError An array with the content or an ApiError on failure
|
||||
*/
|
||||
public function getContent(): array|ApiError
|
||||
{
|
||||
$limit = Config::getInstance()->get(ConfigKey::STORAGE_LIMIT_BYTES) * 2;
|
||||
$body = file_get_contents('php://input', length: $limit + 1);
|
||||
if ($body === false) {
|
||||
return new ApiError(500, "Failed to read request body.");
|
||||
}
|
||||
if (strlen($body) > $limit) {
|
||||
return new ApiError(413, "Request body exceeds maximum allowed size.");
|
||||
}
|
||||
|
||||
$encodingHeader = $_SERVER['HTTP_CONTENT_ENCODING'] ?? '';
|
||||
if ($encodingHeader) {
|
||||
$encodingSteps = explode(',', $encodingHeader);
|
||||
if (count($encodingSteps) > static::MAX_ENCODING_STEPS) {
|
||||
return new ApiError(400, "Too many Content-Encoding steps.");
|
||||
}
|
||||
foreach (array_reverse($encodingSteps) as $step) {
|
||||
switch (trim(strtolower($step))) {
|
||||
case "deflate":
|
||||
$body = @gzinflate($body, $limit);
|
||||
break;
|
||||
case "x-gzip":
|
||||
case "gzip":
|
||||
$body = @gzdecode($body, $limit);
|
||||
break;
|
||||
default:
|
||||
return new ApiError(415, "Unsupported Content-Encoding: " . htmlspecialchars($step));
|
||||
}
|
||||
if ($body === false) {
|
||||
return new ApiError(400, "Failed to decode request body with encoding: " . htmlspecialchars($step));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$contentTypeHeader = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if ($pos = strpos($contentTypeHeader, ';')) {
|
||||
$contentTypeHeader = substr($contentTypeHeader, 0, $pos);
|
||||
}
|
||||
switch ($contentTypeHeader) {
|
||||
case "application/x-www-form-urlencoded":
|
||||
parse_str($body, $data);
|
||||
break;
|
||||
case "application/json":
|
||||
$data = @json_decode($body, true);
|
||||
if (!is_array($data)) {
|
||||
return new ApiError(400, "Failed to parse JSON body.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return new ApiError(415, "Unsupported Content-Type: " . htmlspecialchars($contentTypeHeader));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
33
src/Api/LogContentParser.php
Normal file
33
src/Api/LogContentParser.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api;
|
||||
|
||||
use Aternos\Mclogs\Api\Response\ApiError;
|
||||
|
||||
class LogContentParser extends ContentParser
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getContent(): array|ApiError
|
||||
{
|
||||
$data = parent::getContent();
|
||||
if ($data instanceof ApiError) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (!isset($data['content'])) {
|
||||
return new ApiError(400, "Required field 'content' not found.");
|
||||
}
|
||||
|
||||
if (empty($data['content'])) {
|
||||
return new ApiError(400, "Required field 'content' is empty.");
|
||||
}
|
||||
|
||||
if (!is_string($data['content'])) {
|
||||
return new ApiError(400, "Field 'content' must be a string.");
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
23
src/Api/Response/ApiError.php
Normal file
23
src/Api/Response/ApiError.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
class ApiError extends ApiResponse
|
||||
{
|
||||
protected bool $success = false;
|
||||
|
||||
public function __construct(
|
||||
int $httpCode,
|
||||
protected string $message,
|
||||
)
|
||||
{
|
||||
$this->setHttpCode($httpCode);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = parent::jsonSerialize();
|
||||
$data['error'] = $this->message;
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
63
src/Api/Response/ApiResponse.php
Normal file
63
src/Api/Response/ApiResponse.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
class ApiResponse implements \JsonSerializable
|
||||
{
|
||||
protected int $httpCode = 200;
|
||||
protected bool $success = true;
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $httpCode
|
||||
* @return $this
|
||||
*/
|
||||
public function setHttpCode(int $httpCode): static
|
||||
{
|
||||
$this->httpCode = $httpCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getHttpCode(): int
|
||||
{
|
||||
return $this->httpCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $success
|
||||
* @return $this
|
||||
*/
|
||||
public function setSuccess(bool $success): static
|
||||
{
|
||||
$this->success = $success;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function output(): static
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($this->httpCode);
|
||||
echo json_encode($this, JSON_UNESCAPED_SLASHES);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
17
src/Api/Response/CodexLogResponse.php
Normal file
17
src/Api/Response/CodexLogResponse.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
use Aternos\Codex\Log\LogInterface;
|
||||
|
||||
class CodexLogResponse extends ApiResponse
|
||||
{
|
||||
public function __construct(protected LogInterface $codexLog)
|
||||
{
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), $this->codexLog->jsonSerialize());
|
||||
}
|
||||
}
|
||||
13
src/Api/Response/FiltersResponse.php
Normal file
13
src/Api/Response/FiltersResponse.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
use Aternos\Mclogs\Filter\Filter;
|
||||
|
||||
class FiltersResponse extends ApiResponse
|
||||
{
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return Filter::getAll();
|
||||
}
|
||||
}
|
||||
19
src/Api/Response/LimitsResponse.php
Normal file
19
src/Api/Response/LimitsResponse.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
class LimitsResponse extends ApiResponse
|
||||
{
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$config = Config::getInstance();
|
||||
$data = parent::jsonSerialize();
|
||||
$data['storageTime'] = $config->get(ConfigKey::STORAGE_TTL);
|
||||
$data['maxLength'] = $config->get(ConfigKey::STORAGE_LIMIT_BYTES);
|
||||
$data['maxLines'] = $config->get(ConfigKey::STORAGE_LIMIT_LINES);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
64
src/Api/Response/LogResponse.php
Normal file
64
src/Api/Response/LogResponse.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class LogResponse extends ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
protected Log $log,
|
||||
protected bool $withToken = false,
|
||||
protected bool $withInsights = false,
|
||||
protected bool $withRaw = false,
|
||||
protected bool $withParsed = false)
|
||||
{
|
||||
$this->loadFromGet();
|
||||
}
|
||||
|
||||
public function loadFromGet(): static
|
||||
{
|
||||
$url = URL::getCurrent();
|
||||
$query = $url->getQuery();
|
||||
if (empty($query)) {
|
||||
return $this;
|
||||
}
|
||||
parse_str($url->getQuery(), $get);
|
||||
$this->withInsights = isset($get['insights']);
|
||||
$this->withRaw = isset($get['raw']);
|
||||
$this->withParsed = isset($get['parsed']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$data = parent::jsonSerialize();
|
||||
$data['id'] = $this->log->getId();
|
||||
$data['source'] = $this->log->getSource();
|
||||
$data['created'] = $this->log->getCreated()?->toDateTime()->getTimestamp();
|
||||
$data['expires'] = $this->log->getExpires()?->toDateTime()->getTimestamp();
|
||||
$data['size'] = $this->log->getSize();
|
||||
$data['lines'] = $this->log->getLinesCount();
|
||||
$data['errors'] = $this->log->getErrorsCount();
|
||||
$data['url'] = $this->log->getUrl()->toString();
|
||||
$data['raw'] = $this->log->getRawURL()->toString();
|
||||
if ($this->withToken) {
|
||||
$data['token'] = $this->log->getToken();
|
||||
}
|
||||
$data['metadata'] = $this->log->getMetadata();
|
||||
if ($this->withInsights || $this->withRaw || $this->withParsed) {
|
||||
$data['content'] = [];
|
||||
}
|
||||
if ($this->withInsights) {
|
||||
$data['content']['insights'] = $this->log->getCodexLog()->setIncludeEntries(false);
|
||||
}
|
||||
if ($this->withRaw) {
|
||||
$data['content']['raw'] = $this->log->getContent();
|
||||
}
|
||||
if ($this->withParsed) {
|
||||
$data['content']['parsed'] = $this->log->getCodexLog()->getEntries();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
38
src/Api/Response/MultiResponse.php
Normal file
38
src/Api/Response/MultiResponse.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
class MultiResponse extends ApiResponse
|
||||
{
|
||||
protected int $httpCode = 207;
|
||||
|
||||
/**
|
||||
* @var ApiResponse[]
|
||||
*/
|
||||
protected array $responses = [];
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param ApiResponse $response
|
||||
* @return $this
|
||||
*/
|
||||
public function addResponse(string $id, ApiResponse $response): static
|
||||
{
|
||||
$this->responses[$id] = $response;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$response = parent::jsonSerialize();
|
||||
$results = [];
|
||||
foreach ($this->responses as $id => $apiResponse) {
|
||||
$result = $apiResponse->jsonSerialize();
|
||||
$result["id"] = $id;
|
||||
$result["status"] = $apiResponse->getHttpCode();
|
||||
$results[] = $result;
|
||||
}
|
||||
$response["results"] = $results;
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
22
src/Api/Response/RawLogResponse.php
Normal file
22
src/Api/Response/RawLogResponse.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Api\Response;
|
||||
|
||||
use Aternos\Mclogs\Log;
|
||||
|
||||
class RawLogResponse extends ApiResponse
|
||||
{
|
||||
public function __construct(
|
||||
protected Log $log)
|
||||
{
|
||||
}
|
||||
|
||||
public function output(): static
|
||||
{
|
||||
header('Content-Type: text/plain');
|
||||
echo $this->log->getContent();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
43
src/Cache/CacheEntry.php
Normal file
43
src/Cache/CacheEntry.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Cache;
|
||||
|
||||
use Aternos\Mclogs\Storage\MongoDBClient;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
|
||||
class CacheEntry
|
||||
{
|
||||
public function __construct(protected string $key)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function get(): ?string
|
||||
{
|
||||
$result = MongoDBClient::getInstance()->getCacheCollection()->findOne([
|
||||
"_id" => $this->key
|
||||
]);
|
||||
return $result?->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param int $ttl
|
||||
* @return $this
|
||||
*/
|
||||
public function set(string $data, int $ttl = 24 * 60 * 60): static
|
||||
{
|
||||
MongoDBClient::getInstance()->getCacheCollection()->updateOne(
|
||||
["_id" => $this->key],
|
||||
['$set' => [
|
||||
'data' => $data,
|
||||
'expires' => new UTCDateTime((time() + $ttl) * 1000)
|
||||
]],
|
||||
['upsert' => true]
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
88
src/Config/Config.php
Normal file
88
src/Config/Config.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Config;
|
||||
|
||||
use Aternos\Mclogs\Util\Singleton;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class Config
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
protected array $jsonData = [];
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$configPath = __DIR__ . "/../../config.json";
|
||||
if (file_exists($configPath)) {
|
||||
$jsonContent = file_get_contents($configPath);
|
||||
$data = @json_decode($jsonContent, true);
|
||||
if (is_array($data)) {
|
||||
$this->jsonData = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config value by checking environment variable, then config file, then default value
|
||||
*
|
||||
* @param ConfigKey $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(ConfigKey $key): mixed
|
||||
{
|
||||
$env = getenv($key->getEnvironmentVariable());
|
||||
if ($env !== false) {
|
||||
if ($env === "true") {
|
||||
return true;
|
||||
} else if ($env === "false") {
|
||||
return false;
|
||||
}
|
||||
return $env;
|
||||
}
|
||||
|
||||
$json = $this->getJsonValue($key->getJSONPath());
|
||||
if ($json !== null) {
|
||||
return $json;
|
||||
}
|
||||
|
||||
return $key->getDefaultValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->get(ConfigKey::FRONTEND_NAME) ?? URL::getBase()->getHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get a value from the json data by path
|
||||
*
|
||||
* @param array $path
|
||||
* @param array|null $data
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getJsonValue(array $path, ?array $data = null): mixed
|
||||
{
|
||||
if ($data === null) {
|
||||
$data = $this->jsonData;
|
||||
}
|
||||
|
||||
$nextKey = array_shift($path);
|
||||
|
||||
if (!isset($data[$nextKey])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nextData = $data[$nextKey];
|
||||
if (count($path) === 0) {
|
||||
return $nextData;
|
||||
}
|
||||
if (!is_array($nextData)) {
|
||||
return null;
|
||||
}
|
||||
return $this->getJsonValue($path, $nextData);
|
||||
}
|
||||
}
|
||||
80
src/Config/ConfigKey.php
Normal file
80
src/Config/ConfigKey.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Config;
|
||||
|
||||
enum ConfigKey
|
||||
{
|
||||
case STORAGE_TTL;
|
||||
case STORAGE_LIMIT_BYTES;
|
||||
case STORAGE_LIMIT_LINES;
|
||||
|
||||
case MONGODB_URL;
|
||||
case MONGODB_DATABASE;
|
||||
|
||||
case ID_LENGTH;
|
||||
|
||||
case LEGAL_ABUSE;
|
||||
case LEGAL_IMPRINT;
|
||||
case LEGAL_PRIVACY;
|
||||
|
||||
case FRONTEND_NAME;
|
||||
case FRONTEND_ANALYTICS;
|
||||
case FRONTEND_ASSETS_INTEGRITY;
|
||||
case FRONTEND_COLOR_BACKGROUND;
|
||||
case FRONTEND_COLOR_TEXT;
|
||||
case FRONTEND_COLOR_ACCENT;
|
||||
case FRONTEND_COLOR_ERROR;
|
||||
|
||||
case WORKER_REQUESTS;
|
||||
|
||||
/**
|
||||
* Get the default value for the config key
|
||||
*
|
||||
* @return string|int|null
|
||||
*/
|
||||
public function getDefaultValue(): string|int|null
|
||||
{
|
||||
return match ($this) {
|
||||
ConfigKey::STORAGE_TTL => 90 * 24 * 60 * 60,
|
||||
ConfigKey::STORAGE_LIMIT_BYTES => 10 * 1024 * 1024,
|
||||
ConfigKey::STORAGE_LIMIT_LINES => 25000,
|
||||
|
||||
ConfigKey::MONGODB_URL => 'mongodb://mongo:27017',
|
||||
ConfigKey::MONGODB_DATABASE => 'mclogs',
|
||||
|
||||
ConfigKey::ID_LENGTH => 7,
|
||||
|
||||
ConfigKey::FRONTEND_ANALYTICS => false,
|
||||
|
||||
ConfigKey::FRONTEND_ASSETS_INTEGRITY => true,
|
||||
|
||||
ConfigKey::FRONTEND_COLOR_BACKGROUND => "#1a1a1a",
|
||||
ConfigKey::FRONTEND_COLOR_TEXT => "#e8e8e8",
|
||||
ConfigKey::FRONTEND_COLOR_ACCENT => "#5cb85c",
|
||||
ConfigKey::FRONTEND_COLOR_ERROR => "#f62451",
|
||||
|
||||
ConfigKey::WORKER_REQUESTS => 500,
|
||||
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variable name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEnvironmentVariable(): string
|
||||
{
|
||||
return "MCLOGS_" . $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getJSONPath(): array
|
||||
{
|
||||
$parts = explode("_", $this->name);
|
||||
return array_map(fn($part) => strtolower($part), $parts);
|
||||
}
|
||||
}
|
||||
138
src/Data/Deobfuscator.php
Normal file
138
src/Data/Deobfuscator.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Data;
|
||||
|
||||
use Aternos\Codex\Analysis\Information;
|
||||
use Aternos\Codex\Log\AnalysableLog;
|
||||
use Aternos\Codex\Log\LogInterface;
|
||||
use Aternos\Codex\Minecraft\Analysis\Information\Vanilla\VanillaVersionInformation;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\Fabric\FabricLog;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\VanillaClientLog;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\VanillaCrashReportLog;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\VanillaLog;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\VanillaNetworkProtocolErrorReportLog;
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\Vanilla\VanillaServerLog;
|
||||
use Aternos\Mclogs\Cache\CacheEntry;
|
||||
use Aternos\Sherlock\MapLocator\FabricMavenMapLocator;
|
||||
use Aternos\Sherlock\MapLocator\LauncherMetaMapLocator;
|
||||
use Aternos\Sherlock\Maps\GZURLYarnMap;
|
||||
use Aternos\Sherlock\Maps\ObfuscationMap;
|
||||
use Aternos\Sherlock\Maps\URLVanillaObfuscationMap;
|
||||
use Aternos\Sherlock\Maps\VanillaObfuscationMap;
|
||||
use Aternos\Sherlock\Maps\YarnMap;
|
||||
use Aternos\Sherlock\ObfuscatedString;
|
||||
use Exception;
|
||||
|
||||
class Deobfuscator
|
||||
{
|
||||
public function __construct(protected LogInterface $codexLog)
|
||||
{
|
||||
}
|
||||
|
||||
public function deobfuscate(): ?string
|
||||
{
|
||||
if (!$this->codexLog instanceof AnalysableLog) {
|
||||
return null;
|
||||
}
|
||||
if (!$this->codexLog instanceof VanillaLog) {
|
||||
return null;
|
||||
}
|
||||
$analysis = $this->codexLog->analyse();
|
||||
|
||||
/**
|
||||
* @var ?Information $version
|
||||
*/
|
||||
$version = $analysis->getFilteredInsights(VanillaVersionInformation::class)[0] ?? null;
|
||||
if (!$version) {
|
||||
return null;
|
||||
}
|
||||
$version = $version->getValue();
|
||||
|
||||
try {
|
||||
$map = $this->getObfuscationMap($version);
|
||||
} catch (Exception) {
|
||||
$map = null;
|
||||
}
|
||||
|
||||
if ($map === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$obfuscatedContent = new ObfuscatedString($this->codexLog->getLogFile()->getContent(), $map);
|
||||
if ($content = $obfuscatedContent->getMappedContent()) {
|
||||
return $content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the obfuscation map matching this log
|
||||
*
|
||||
* @param $version
|
||||
* @return ObfuscationMap|null
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getObfuscationMap($version): ?ObfuscationMap
|
||||
{
|
||||
if (in_array(get_class($this->codexLog), [
|
||||
VanillaServerLog::class,
|
||||
VanillaClientLog::class,
|
||||
VanillaCrashReportLog::class,
|
||||
VanillaNetworkProtocolErrorReportLog::class
|
||||
])) {
|
||||
$urlCache = new CacheEntry("sherlock:vanilla:$version:client");
|
||||
|
||||
$mapURL = $urlCache->get();
|
||||
if (!$mapURL) {
|
||||
$mapURL = new LauncherMetaMapLocator($version, "client")->findMappingURL();
|
||||
|
||||
if (!$mapURL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$urlCache->set($mapURL, 30 * 24 * 60 * 60);
|
||||
}
|
||||
|
||||
try {
|
||||
$mapCache = new CacheEntry("sherlock:$mapURL");
|
||||
if ($mapContent = $mapCache->get()) {
|
||||
$map = new VanillaObfuscationMap($mapContent);
|
||||
} else {
|
||||
$map = new URLVanillaObfuscationMap($mapURL);
|
||||
$mapCache->set($map->getContent());
|
||||
}
|
||||
} catch (Exception) {
|
||||
}
|
||||
return $map ?? null;
|
||||
}
|
||||
|
||||
if ($this->codexLog instanceof FabricLog) {
|
||||
$urlCache = new CacheEntry("sherlock:yarn:$version:server");
|
||||
|
||||
$mapURL = $urlCache->get();
|
||||
if (!$mapURL) {
|
||||
$mapURL = new FabricMavenMapLocator($version)->findMappingURL();
|
||||
|
||||
if (!$mapURL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$urlCache->set($mapURL, 24 * 60 * 60);
|
||||
}
|
||||
|
||||
try {
|
||||
$mapCache = new CacheEntry("sherlock:$mapURL");
|
||||
if ($mapContent = $mapCache->get()) {
|
||||
$map = new YarnMap($mapContent);
|
||||
} else {
|
||||
$map = new GZURLYarnMap($mapURL);
|
||||
$mapCache->set($map->getContent());
|
||||
}
|
||||
} catch (Exception) {
|
||||
}
|
||||
return $map ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
198
src/Data/MetadataEntry.php
Normal file
198
src/Data/MetadataEntry.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Data;
|
||||
|
||||
use MongoDB\BSON\Serializable;
|
||||
use MongoDB\Model\BSONDocument;
|
||||
|
||||
class MetadataEntry implements \JsonSerializable, Serializable
|
||||
{
|
||||
public const int MAX_ENTRIES = 100;
|
||||
protected const int MAX_KEY_LENGTH = 64;
|
||||
protected const int MAX_LABEL_LENGTH = 128;
|
||||
protected const int MAX_VALUE_LENGTH = 1024;
|
||||
|
||||
protected ?string $key = null;
|
||||
protected mixed $value = null;
|
||||
protected ?string $label = null;
|
||||
protected bool $visible = true;
|
||||
|
||||
/**
|
||||
* @param iterable|null $dataArray
|
||||
* @return MetadataEntry[]
|
||||
*/
|
||||
public static function allFromArray(?iterable $dataArray): array
|
||||
{
|
||||
if ($dataArray === null) {
|
||||
return [];
|
||||
}
|
||||
$entries = [];
|
||||
foreach ($dataArray as $data) {
|
||||
if (is_array($data)) {
|
||||
$entry = static::fromArray($data);
|
||||
} else if (is_object($data)) {
|
||||
$entry = static::fromObject($data);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if ($entry !== null) {
|
||||
$entries[] = $entry;
|
||||
}
|
||||
if (count($entries) >= static::MAX_ENTRIES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return MetadataEntry|null
|
||||
*/
|
||||
public static function fromArray(array $data): ?MetadataEntry
|
||||
{
|
||||
$entry = new MetadataEntry()->setFromArray($data);
|
||||
if (!$entry->isValid()) {
|
||||
return null;
|
||||
}
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $data
|
||||
* @return MetadataEntry|null
|
||||
*/
|
||||
public static function fromObject(object $data): ?MetadataEntry
|
||||
{
|
||||
if ($data instanceof BSONDocument) {
|
||||
$arrayData = $data->getArrayCopy();
|
||||
} else {
|
||||
$arrayData = get_object_vars($data);
|
||||
}
|
||||
return static::fromArray($arrayData);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
"key" => $this->key,
|
||||
"value" => $this->value,
|
||||
"label" => $this->label,
|
||||
"visible" => $this->visible,
|
||||
];
|
||||
}
|
||||
|
||||
public function bsonSerialize(): array
|
||||
{
|
||||
return $this->jsonSerialize();
|
||||
}
|
||||
|
||||
public function getKey(): ?string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function setKey(?string $key): static
|
||||
{
|
||||
if (is_string($key) && strlen($key) > static::MAX_KEY_LENGTH) {
|
||||
$key = substr($key, 0, static::MAX_KEY_LENGTH);
|
||||
}
|
||||
$this->key = $key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setValue(mixed $value): static
|
||||
{
|
||||
if (is_string($value)) {
|
||||
if (strlen($value) > static::MAX_VALUE_LENGTH) {
|
||||
$value = substr($value, 0, static::MAX_VALUE_LENGTH);
|
||||
}
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
if (is_int($value) || is_float($value) || is_bool($value) || is_null($value)) {
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
$encodedValue = @json_encode($value);
|
||||
if ($encodedValue === false) {
|
||||
$this->value = null;
|
||||
return $this;
|
||||
}
|
||||
if (strlen($encodedValue) > static::MAX_VALUE_LENGTH) {
|
||||
$encodedValue = substr($encodedValue, 0, static::MAX_VALUE_LENGTH);
|
||||
}
|
||||
$this->value = $encodedValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getDisplayLabel(): ?string
|
||||
{
|
||||
return $this->label ?? $this->key;
|
||||
}
|
||||
|
||||
public function getDisplayValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setLabel(?string $label): static
|
||||
{
|
||||
if (is_string($label) && strlen($label) > static::MAX_LABEL_LENGTH) {
|
||||
$label = substr($label, 0, static::MAX_LABEL_LENGTH);
|
||||
}
|
||||
$this->label = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setVisible(bool $visible): static
|
||||
{
|
||||
$this->visible = $visible;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->key !== null && $this->value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setFromArray(array $data): static
|
||||
{
|
||||
if (isset($data['key']) && is_string($data['key'])) {
|
||||
$this->setKey($data['key']);
|
||||
}
|
||||
if (isset($data['value'])) {
|
||||
$this->setValue($data['value']);
|
||||
}
|
||||
if (isset($data['label']) && is_string($data['label'])) {
|
||||
$this->setLabel($data['label']);
|
||||
}
|
||||
if (isset($data['visible']) && is_bool($data['visible'])) {
|
||||
$this->setVisible($data['visible']);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
42
src/Data/Token.php
Normal file
42
src/Data/Token.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Data;
|
||||
|
||||
use Random\RandomException;
|
||||
|
||||
class Token implements \JsonSerializable
|
||||
{
|
||||
public function __construct(protected ?string $value = null)
|
||||
{
|
||||
if ($this->value === null) {
|
||||
$this->generate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
public function matches(string $token): bool
|
||||
{
|
||||
return hash_equals($this->value, $token);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
protected function generate(): void
|
||||
{
|
||||
$this->value = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
public function get(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
16
src/Detective.php
Normal file
16
src/Detective.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs;
|
||||
|
||||
use Aternos\Codex\Minecraft\Log\Minecraft\MinecraftLog;
|
||||
|
||||
class Detective extends \Aternos\Codex\Detective\Detective
|
||||
{
|
||||
protected string $defaultLogClass = MinecraftLog::class;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addDetective(new \Aternos\Codex\Minecraft\Detective\Detective())
|
||||
->addDetective(new \Aternos\Codex\Hytale\Detective\Detective());
|
||||
}
|
||||
}
|
||||
21
src/Filter/AccessTokenFilter.php
Normal file
21
src/Filter/AccessTokenFilter.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Filter\Pattern\PatternWithReplacement;
|
||||
|
||||
class AccessTokenFilter extends RegexFilter
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getPatterns(): array
|
||||
{
|
||||
return [
|
||||
new PatternWithReplacement('\(Session ID is token:[^:]+\:[^)]+\)', '(Session ID is token:****************:****************)'),
|
||||
new PatternWithReplacement('--accessToken [^ ]+', '--accessToken ****************:****************'),
|
||||
new PatternWithReplacement('"authToken":"[^"]+"', '"authToken":"****************"'),
|
||||
new PatternWithReplacement('"refreshToken":"[^"]+"', '"refreshToken":"****************"'),
|
||||
];
|
||||
}
|
||||
}
|
||||
75
src/Filter/Filter.php
Normal file
75
src/Filter/Filter.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
abstract class Filter implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var Filter[]|null
|
||||
*/
|
||||
protected static ?array $filter = null;
|
||||
|
||||
/**
|
||||
* Get all filters
|
||||
*
|
||||
* @return Filter[]
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
if (static::$filter !== null) {
|
||||
return static::$filter;
|
||||
}
|
||||
return static::$filter = [
|
||||
new TrimFilter(),
|
||||
new LimitBytesFilter(),
|
||||
new LimitLinesFilter(),
|
||||
new IPv4Filter(),
|
||||
new IPv6Filter(),
|
||||
new UsernameFilter(),
|
||||
new AccessTokenFilter(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the $data string with all filters and return it
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public static function filterAll(string $data): string
|
||||
{
|
||||
foreach (static::getAll() as $filter) {
|
||||
$data = $filter->filter($data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FilterType
|
||||
*/
|
||||
abstract public function getType(): FilterType;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getData(): mixed;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
"type" => $this->getType()->value,
|
||||
"data" => $this->getData(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the $data string and return it
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
abstract public function filter(string $data): string;
|
||||
}
|
||||
11
src/Filter/FilterType.php
Normal file
11
src/Filter/FilterType.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
enum FilterType: string
|
||||
{
|
||||
case TRIM = 'trim';
|
||||
case LIMIT_BYTES = 'limit-bytes';
|
||||
case LIMIT_LINES = 'limit-lines';
|
||||
case REGEX = 'regex';
|
||||
}
|
||||
32
src/Filter/IPv4Filter.php
Normal file
32
src/Filter/IPv4Filter.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Filter\Pattern\Pattern;
|
||||
use Aternos\Mclogs\Filter\Pattern\PatternWithReplacement;
|
||||
|
||||
class IPv4Filter extends RegexFilter
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getPatterns(): array
|
||||
{
|
||||
return [
|
||||
new PatternWithReplacement('(?<!version:? )(?<!([0-9]|-|\w))([0-9]{1,3}\.){3}[0-9]{1,3}(?![0-9])', '**.**.**.**'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getExemptions(): array
|
||||
{
|
||||
return [
|
||||
new Pattern('^127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'),
|
||||
new Pattern('^0\.0\.0\.0$'),
|
||||
new Pattern('^1\.[01]\.[01]\.1$'),
|
||||
new Pattern('^8\.8\.[84]\.[84]$'),
|
||||
];
|
||||
}
|
||||
}
|
||||
30
src/Filter/IPv6Filter.php
Normal file
30
src/Filter/IPv6Filter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Filter\Pattern\Pattern;
|
||||
use Aternos\Mclogs\Filter\Pattern\PatternWithReplacement;
|
||||
|
||||
class IPv6Filter extends RegexFilter
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getPatterns(): array
|
||||
{
|
||||
return [
|
||||
new PatternWithReplacement('(?<=^|\W)((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?(?=$|\W)',
|
||||
'****:****:****:****:****:****:****:****')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getExemptions(): array
|
||||
{
|
||||
return [
|
||||
new Pattern('^[0:]+1?$')
|
||||
];
|
||||
}
|
||||
}
|
||||
41
src/Filter/LimitBytesFilter.php
Normal file
41
src/Filter/LimitBytesFilter.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
class LimitBytesFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* Filter the $data string and return it
|
||||
*
|
||||
* Cuts the length down to maxLength
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function filter(string $data): string
|
||||
{
|
||||
$lengthLimit = Config::getInstance()->get(ConfigKey::STORAGE_LIMIT_BYTES);
|
||||
return mb_strcut($data, 0, $lengthLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FilterType
|
||||
*/
|
||||
public function getType(): FilterType
|
||||
{
|
||||
return FilterType::LIMIT_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return [
|
||||
"limit" => Config::getInstance()->get(ConfigKey::STORAGE_LIMIT_BYTES)
|
||||
];
|
||||
}
|
||||
}
|
||||
41
src/Filter/LimitLinesFilter.php
Normal file
41
src/Filter/LimitLinesFilter.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
class LimitLinesFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* Filter the $data string and return it
|
||||
*
|
||||
* Cuts the lines down to maxLines
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function filter(string $data): string
|
||||
{
|
||||
$linesLimit = Config::getInstance()->get(ConfigKey::STORAGE_LIMIT_LINES);
|
||||
return implode("\n", array_slice(explode("\n", $data), 0, $linesLimit));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FilterType
|
||||
*/
|
||||
public function getType(): FilterType
|
||||
{
|
||||
return FilterType::LIMIT_LINES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return [
|
||||
"limit" => Config::getInstance()->get(ConfigKey::STORAGE_LIMIT_LINES)
|
||||
];
|
||||
}
|
||||
}
|
||||
16
src/Filter/Pattern/Modifier.php
Normal file
16
src/Filter/Pattern/Modifier.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter\Pattern;
|
||||
|
||||
/**
|
||||
* https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php
|
||||
*/
|
||||
enum Modifier: string implements \JsonSerializable
|
||||
{
|
||||
case CASELESS = 'i';
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
51
src/Filter/Pattern/Pattern.php
Normal file
51
src/Filter/Pattern/Pattern.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter\Pattern;
|
||||
|
||||
class Pattern implements \JsonSerializable
|
||||
{
|
||||
protected const string DELIMITER = '/';
|
||||
|
||||
/**
|
||||
* @param string $pattern
|
||||
* @param Modifier[] $modifiers
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $pattern,
|
||||
protected array $modifiers = [Modifier::CASELESS]
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full regex pattern with delimiters and modifiers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get(): string
|
||||
{
|
||||
$modifiersString = '';
|
||||
foreach ($this->modifiers as $modifier) {
|
||||
$modifiersString .= $modifier->value;
|
||||
}
|
||||
return static::DELIMITER . $this->pattern . static::DELIMITER . $modifiersString;
|
||||
}
|
||||
|
||||
public function getPattern(): string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public function getModifiers(): array
|
||||
{
|
||||
return $this->modifiers;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'pattern' => $this->getPattern(),
|
||||
'modifiers' => $this->getModifiers()
|
||||
];
|
||||
}
|
||||
}
|
||||
26
src/Filter/Pattern/PatternWithReplacement.php
Normal file
26
src/Filter/Pattern/PatternWithReplacement.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter\Pattern;
|
||||
|
||||
class PatternWithReplacement extends Pattern
|
||||
{
|
||||
public function __construct(string $pattern, protected string $replacement, array $modifiers = [Modifier::CASELESS])
|
||||
{
|
||||
parent::__construct($pattern, $modifiers);
|
||||
}
|
||||
|
||||
public function getReplacement(): string
|
||||
{
|
||||
return $this->replacement;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::jsonSerialize(),
|
||||
[
|
||||
'replacement' => $this->getReplacement()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
59
src/Filter/RegexFilter.php
Normal file
59
src/Filter/RegexFilter.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Filter\Pattern\Pattern;
|
||||
use Aternos\Mclogs\Filter\Pattern\PatternWithReplacement;
|
||||
|
||||
abstract class RegexFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* @return PatternWithReplacement[]
|
||||
*/
|
||||
abstract protected function getPatterns(): array;
|
||||
|
||||
/**
|
||||
* @return Pattern[]
|
||||
*/
|
||||
protected function getExemptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType(): FilterType
|
||||
{
|
||||
return FilterType::REGEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return [
|
||||
"patterns" => $this->getPatterns(),
|
||||
"exemptions" => $this->getExemptions(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function filter(string $data): string
|
||||
{
|
||||
foreach ($this->getPatterns() as $pattern) {
|
||||
$data = preg_replace_callback($pattern->get(), function ($matches) use ($pattern) {
|
||||
foreach ($this->getExemptions() as $exemptionPattern) {
|
||||
if (preg_match($exemptionPattern->get(), $matches[0])) {
|
||||
return $matches[0];
|
||||
}
|
||||
}
|
||||
return $pattern->getReplacement();
|
||||
}, $data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
29
src/Filter/TrimFilter.php
Normal file
29
src/Filter/TrimFilter.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
class TrimFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* Filter the $data string and return it
|
||||
*
|
||||
* Trim pre and after whitespace
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
public function filter(string $data): string
|
||||
{
|
||||
return trim($data);
|
||||
}
|
||||
|
||||
public function getType(): FilterType
|
||||
{
|
||||
return FilterType::TRIM;
|
||||
}
|
||||
|
||||
public function getData(): object
|
||||
{
|
||||
return new \stdClass();
|
||||
}
|
||||
}
|
||||
23
src/Filter/UsernameFilter.php
Normal file
23
src/Filter/UsernameFilter.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Filter;
|
||||
|
||||
use Aternos\Mclogs\Filter\Pattern\PatternWithReplacement;
|
||||
|
||||
class UsernameFilter extends RegexFilter
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getPatterns(): array
|
||||
{
|
||||
return [
|
||||
new PatternWithReplacement("C:\\\\Users\\\\([^\\\\]+)\\\\", "C:\\Users\\********\\"), // windows
|
||||
new PatternWithReplacement("C:\\\\\\\\Users\\\\\\\\([^\\\\]+)\\\\\\\\", "C:\\\\Users\\\\********\\\\"), // windows with double backslashes
|
||||
new PatternWithReplacement("C:\\/Users\\/([^\\/]+)\\/", "C:/Users/********/"), // windows with forward slashes
|
||||
new PatternWithReplacement("(?<!\\w)\\/home\\/[^\\/]+\\/", "/home/********/"), // linux
|
||||
new PatternWithReplacement("(?<!\\w)\\/Users\\/[^\\/]+\\/", "/Users/********/"), // macos
|
||||
new PatternWithReplacement("USERNAME=\\w+", "USERNAME=********"), // environment variable
|
||||
];
|
||||
}
|
||||
}
|
||||
14
src/Frontend/Action/ApiDocsAction.php
Normal file
14
src/Frontend/Action/ApiDocsAction.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
class ApiDocsAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
require __DIR__ . "/../../../web/frontend/api-docs.php";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
21
src/Frontend/Action/CreateLogAction.php
Normal file
21
src/Frontend/Action/CreateLogAction.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class CreateLogAction extends \Aternos\Mclogs\Api\Action\CreateLogAction
|
||||
{
|
||||
protected bool $includeCookie = true;
|
||||
protected bool $includeToken = false;
|
||||
|
||||
protected function getAllowedOrigin(): string
|
||||
{
|
||||
return URL::getBase()->toString();
|
||||
}
|
||||
|
||||
protected function shouldAllowCredentials(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
30
src/Frontend/Action/DeleteLogAction.php
Normal file
30
src/Frontend/Action/DeleteLogAction.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Frontend\Cookie\TokenCookie;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class DeleteLogAction extends \Aternos\Mclogs\Api\Action\DeleteLogAction
|
||||
{
|
||||
protected function getAllowedOrigin(): string
|
||||
{
|
||||
return URL::getBase()->toString();
|
||||
}
|
||||
|
||||
protected function shouldAllowCredentials(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getRequestToken(): ?string
|
||||
{
|
||||
return new TokenCookie()->getValue();
|
||||
}
|
||||
|
||||
protected function handleDeletedLog(Log $log): void
|
||||
{
|
||||
new TokenCookie()->setLog($log)->delete();
|
||||
}
|
||||
}
|
||||
15
src/Frontend/Action/FaviconAction.php
Normal file
15
src/Frontend/Action/FaviconAction.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
class FaviconAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
header('Content-Type: image/svg+xml');
|
||||
require __DIR__ . "/../../../web/frontend/parts/favicon.php";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
15
src/Frontend/Action/NotFoundAction.php
Normal file
15
src/Frontend/Action/NotFoundAction.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
class NotFoundAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
http_response_code(404);
|
||||
require __DIR__ . "/../../../web/frontend/404.php";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
14
src/Frontend/Action/StartAction.php
Normal file
14
src/Frontend/Action/StartAction.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
|
||||
class StartAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
require __DIR__ . "/../../../web/frontend/start.php";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
src/Frontend/Action/ViewLogAction.php
Normal file
25
src/Frontend/Action/ViewLogAction.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Action;
|
||||
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Log;
|
||||
use Aternos\Mclogs\Router\Action;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class ViewLogAction extends Action
|
||||
{
|
||||
public function run(): bool
|
||||
{
|
||||
$id = new Id(URL::getLastPathPart());
|
||||
$log = Log::find($id);
|
||||
if (!$log) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$log->renew();
|
||||
|
||||
require __DIR__ . "/../../../web/frontend/log.php";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
105
src/Frontend/Assets/Asset.php
Normal file
105
src/Frontend/Assets/Asset.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Assets;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
class Asset implements \JsonSerializable
|
||||
{
|
||||
protected const string HASH_ALGORITHM = 'sha384';
|
||||
|
||||
/**
|
||||
* @param object $data
|
||||
* @return static|null
|
||||
*/
|
||||
public static function fromObject(object $data): ?static
|
||||
{
|
||||
if (!isset($data->type) || !isset($data->path) || !isset($data->hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = AssetType::tryFrom($data->type);
|
||||
if ($type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static($type, $data->path, $data->hash);
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
protected AssetType $type,
|
||||
protected string $path,
|
||||
protected ?string $hash = null)
|
||||
{
|
||||
$this->path = ltrim($this->path, '/');
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getPathWithVersion(): string
|
||||
{
|
||||
return $this->path . '?v=' . rawurlencode(substr($this->getHash(), 0, 16));
|
||||
}
|
||||
|
||||
protected function getAbsoluteBasePath(): string
|
||||
{
|
||||
return __DIR__ . "/../../../web/public/";
|
||||
}
|
||||
|
||||
protected function getAbsolutePath(): string
|
||||
{
|
||||
return $this->getAbsoluteBasePath() . $this->path;
|
||||
}
|
||||
|
||||
protected function buildHash(): string
|
||||
{
|
||||
return hash_file(static::HASH_ALGORITHM, $this->getAbsolutePath());
|
||||
}
|
||||
|
||||
protected function getHash(): string
|
||||
{
|
||||
if ($this->hash === null) {
|
||||
return $this->buildHash();
|
||||
}
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
protected function getBase64Hash(): string
|
||||
{
|
||||
return base64_encode(hex2bin($this->getHash()));
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType()->value,
|
||||
'path' => $this->getPath(),
|
||||
'hash' => $this->getHash(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getType(): AssetType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getHTML(): string
|
||||
{
|
||||
return match ($this->type) {
|
||||
AssetType::CSS => '<link rel="stylesheet" href="/' . $this->getPathWithVersion() . '"' . $this->getIntegrityAttribute() . ' />',
|
||||
AssetType::JS => '<script src="/' . $this->getPathWithVersion() . '"' . $this->getIntegrityAttribute() . '></script>'
|
||||
};
|
||||
}
|
||||
|
||||
protected function getIntegrityAttribute(): string
|
||||
{
|
||||
if (!Config::getInstance()->get(ConfigKey::FRONTEND_ASSETS_INTEGRITY)) {
|
||||
return '';
|
||||
}
|
||||
return ' integrity="' . static::HASH_ALGORITHM . '-' . $this->getBase64Hash() . '"';
|
||||
}
|
||||
}
|
||||
101
src/Frontend/Assets/AssetLoader.php
Normal file
101
src/Frontend/Assets/AssetLoader.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Assets;
|
||||
|
||||
use Aternos\Mclogs\Util\Singleton;
|
||||
|
||||
class AssetLoader
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
protected const string CACHE_PATH = __DIR__ . "/../../../assets.cache";
|
||||
|
||||
/**
|
||||
* @var Asset[]
|
||||
*/
|
||||
protected array $cachedAssets = [];
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$this->loadCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AssetType $assetType
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public function getHTML(AssetType $assetType, string $path): string
|
||||
{
|
||||
return $this->getAsset($assetType, $path)->getHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AssetType $assetType
|
||||
* @param string $path
|
||||
* @return Asset
|
||||
*/
|
||||
protected function getAsset(AssetType $assetType, string $path): Asset
|
||||
{
|
||||
$cachedAsset = $this->findCachedAsset($assetType, $path);
|
||||
if ($cachedAsset !== null) {
|
||||
return $cachedAsset;
|
||||
}
|
||||
return new Asset($assetType, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AssetType $assetType
|
||||
* @param string $path
|
||||
* @return Asset|null
|
||||
*/
|
||||
protected function findCachedAsset(AssetType $assetType, string $path): ?Asset
|
||||
{
|
||||
foreach ($this->cachedAssets as $asset) {
|
||||
if ($asset->getPath() === $path && $asset->getType() === $assetType) {
|
||||
return $asset;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function loadCache(): void
|
||||
{
|
||||
if (!file_exists(self::CACHE_PATH)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents(self::CACHE_PATH);
|
||||
if ($content === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode($content);
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($data as $assetData) {
|
||||
if (!is_object($assetData)) {
|
||||
continue;
|
||||
}
|
||||
$asset = Asset::fromObject($assetData);
|
||||
if ($asset === null) {
|
||||
continue;
|
||||
}
|
||||
$this->cachedAssets[] = $asset;
|
||||
}
|
||||
}
|
||||
|
||||
public function writeCache(): void
|
||||
{
|
||||
$assets = [
|
||||
new Asset(AssetType::CSS, "css/mclogs.css"),
|
||||
new Asset(AssetType::JS, "js/start.js"),
|
||||
new Asset(AssetType::JS, "js/log.js"),
|
||||
new Asset(AssetType::CSS, "vendor/fontawesome/css/fontawesome.min.css")
|
||||
];
|
||||
|
||||
file_put_contents(static::CACHE_PATH, json_encode($assets));
|
||||
}
|
||||
}
|
||||
9
src/Frontend/Assets/AssetType.php
Normal file
9
src/Frontend/Assets/AssetType.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Assets;
|
||||
|
||||
enum AssetType: string
|
||||
{
|
||||
case CSS = "css";
|
||||
case JS = "js";
|
||||
}
|
||||
139
src/Frontend/Cookie/Cookie.php
Normal file
139
src/Frontend/Cookie/Cookie.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Cookie;
|
||||
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
abstract class Cookie
|
||||
{
|
||||
protected ?string $value = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getKey(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getDomain(): string
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
protected function getMaxAge(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getPath(): string
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function isSecure(): bool
|
||||
{
|
||||
return URL::getCurrent()->getScheme() === "https";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function isHttpOnly(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getSameSite(): string
|
||||
{
|
||||
return "Lax";
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->value = $_COOKIE[$this->getKey()] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $value): bool
|
||||
{
|
||||
$options = [
|
||||
'expires' => $this->getMaxAge() !== null ? time() + $this->getMaxAge() : 0,
|
||||
'path' => $this->getPath(),
|
||||
'domain' => $this->getDomain(),
|
||||
'secure' => $this->isSecure(),
|
||||
'httponly' => $this->isHttpOnly(),
|
||||
'samesite' => $this->getSameSite()
|
||||
];
|
||||
|
||||
$result = setcookie(
|
||||
$this->getKey(),
|
||||
$value,
|
||||
$options
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
$options = [
|
||||
'expires' => time() - 3600,
|
||||
'path' => $this->getPath(),
|
||||
'domain' => $this->getDomain(),
|
||||
'secure' => $this->isSecure(),
|
||||
'httponly' => $this->isHttpOnly(),
|
||||
'samesite' => $this->getSameSite()
|
||||
];
|
||||
|
||||
$result = setcookie(
|
||||
$this->getKey(),
|
||||
'',
|
||||
$options
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$this->value = null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->getValue() !== null;
|
||||
}
|
||||
}
|
||||
14
src/Frontend/Cookie/SettingsCookie.php
Normal file
14
src/Frontend/Cookie/SettingsCookie.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Cookie;
|
||||
|
||||
class SettingsCookie extends Cookie
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getKey(): string
|
||||
{
|
||||
return "MCLOGS_SETTINGS";
|
||||
}
|
||||
}
|
||||
52
src/Frontend/Cookie/TokenCookie.php
Normal file
52
src/Frontend/Cookie/TokenCookie.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Cookie;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
use Aternos\Mclogs\Log;
|
||||
|
||||
class TokenCookie extends Cookie
|
||||
{
|
||||
/**
|
||||
* @param Log $log
|
||||
* @return $this
|
||||
*/
|
||||
public function setLog(Log $log): static
|
||||
{
|
||||
$this->log = $log;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getKey(): string
|
||||
{
|
||||
return "MCLOGS_LOG_TOKEN";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Log|null $log
|
||||
*/
|
||||
public function __construct(protected ?Log $log = null)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getPath(): string
|
||||
{
|
||||
if (!$this->log) {
|
||||
return "/";
|
||||
}
|
||||
return "/" . $this->log->getId()->get();
|
||||
}
|
||||
|
||||
protected function getMaxAge(): ?int
|
||||
{
|
||||
return Config::getInstance()->get(ConfigKey::STORAGE_TTL);
|
||||
}
|
||||
}
|
||||
21
src/Frontend/FrontendRouter.php
Normal file
21
src/Frontend/FrontendRouter.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend;
|
||||
|
||||
use Aternos\Mclogs\Router\Router;
|
||||
use Aternos\Mclogs\Id;
|
||||
use Aternos\Mclogs\Router\Method;
|
||||
|
||||
class FrontendRouter extends Router
|
||||
{
|
||||
protected function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->register(Method::GET, "#^/$#", new Action\StartAction())
|
||||
->register(Method::GET, "#^/" . Id::PATTERN . "$#", new Action\ViewLogAction())
|
||||
->register(Method::POST, "#^/new$#", new Action\CreateLogAction())
|
||||
->register(Method::DELETE, "#^/" . Id::PATTERN . "$#", new Action\DeleteLogAction())
|
||||
->register(Method::GET, "#^/favicon\.svg$#", new Action\FaviconAction())
|
||||
->setDefaultAction(new Action\NotFoundAction());
|
||||
}
|
||||
}
|
||||
39
src/Frontend/Settings/Setting.php
Normal file
39
src/Frontend/Settings/Setting.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Settings;
|
||||
|
||||
enum Setting: string
|
||||
{
|
||||
case FULL_WIDTH = "fullWidth";
|
||||
case NO_WRAP = "noWrap";
|
||||
case FLOATING_SCROLLBAR = "floatingScrollbar";
|
||||
case OVERFLOW = "overflow";
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
Setting::FULL_WIDTH => "Full Width",
|
||||
Setting::NO_WRAP => "No Wrap",
|
||||
Setting::FLOATING_SCROLLBAR => "Floating Scrollbar",
|
||||
Setting::OVERFLOW => "Overflow"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
function getBodyClass(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
Setting::FULL_WIDTH => "setting-full-width",
|
||||
Setting::NO_WRAP => "setting-no-wrap",
|
||||
Setting::FLOATING_SCROLLBAR => "setting-floating-scrollbar",
|
||||
Setting::OVERFLOW => "setting-overflow",
|
||||
default => null
|
||||
};
|
||||
}
|
||||
}
|
||||
66
src/Frontend/Settings/Settings.php
Normal file
66
src/Frontend/Settings/Settings.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Frontend\Settings;
|
||||
|
||||
use Aternos\Mclogs\Frontend\Cookie\SettingsCookie;
|
||||
|
||||
class Settings
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$rawData = new SettingsCookie()->getValue();
|
||||
if ($rawData) {
|
||||
$parsedData = json_decode($rawData, true);
|
||||
if (is_array($parsedData)) {
|
||||
$this->data = $parsedData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Setting $key
|
||||
* @return bool
|
||||
*/
|
||||
public function get(Setting $key): bool
|
||||
{
|
||||
$value = $this->data[$key->value] ?? false;
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getBodyClasses(): array
|
||||
{
|
||||
$classes = [];
|
||||
foreach (Setting::cases() as $setting) {
|
||||
if ($this->get($setting)) {
|
||||
$bodyClass = $setting->getBodyClass();
|
||||
if ($bodyClass) {
|
||||
$classes[] = $bodyClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBodyClassesString(): string
|
||||
{
|
||||
$classes = $this->getBodyClasses();
|
||||
if (empty($classes)) {
|
||||
return "";
|
||||
}
|
||||
return " " . implode(" ", $this->getBodyClasses());
|
||||
}
|
||||
}
|
||||
60
src/Id.php
Normal file
60
src/Id.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs;
|
||||
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
|
||||
class Id implements \JsonSerializable
|
||||
{
|
||||
public const string PATTERN = '[a-zA-Z0-9]+';
|
||||
protected const string CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
/**
|
||||
* @param string|null $id
|
||||
*/
|
||||
public function __construct(protected ?string $id = null)
|
||||
{
|
||||
if ($this->id === null) {
|
||||
$this->generate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generate(): string
|
||||
{
|
||||
$config = \Aternos\Mclogs\Config\Config::getInstance();
|
||||
$idLength = $config->get(ConfigKey::ID_LENGTH);
|
||||
|
||||
$newId = "";
|
||||
for ($i = 0; $i < $idLength; $i++) {
|
||||
$newId .= static::CHARACTERS[rand(0, strlen(static::CHARACTERS) - 1)];
|
||||
}
|
||||
|
||||
return $this->id = $newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
516
src/Log.php
Normal file
516
src/Log.php
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs;
|
||||
|
||||
use Aternos\Codex\Analysis\Analysis;
|
||||
use Aternos\Codex\Log\AnalysableLogInterface;
|
||||
use Aternos\Codex\Log\File\StringLogFile;
|
||||
use Aternos\Codex\Log\Level;
|
||||
use Aternos\Codex\Log\LogInterface;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
use Aternos\Mclogs\Data\Deobfuscator;
|
||||
use Aternos\Mclogs\Data\MetadataEntry;
|
||||
use Aternos\Mclogs\Data\Token;
|
||||
use Aternos\Mclogs\Filter\Filter;
|
||||
use Aternos\Mclogs\Frontend\Cookie\TokenCookie;
|
||||
use Aternos\Mclogs\Printer\Printer;
|
||||
use Aternos\Mclogs\Storage\MongoDBClient;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
class Log
|
||||
{
|
||||
protected const int SOURCE_MAX_LENGTH = 64;
|
||||
|
||||
protected ?string $source = null;
|
||||
protected ?UTCDateTime $expires = null;
|
||||
protected ?UTCDateTime $created = null;
|
||||
protected ?Token $token = null;
|
||||
|
||||
/**
|
||||
* @var MetadataEntry[]
|
||||
*/
|
||||
protected array $metadata = [];
|
||||
|
||||
protected ?LogInterface $log = null;
|
||||
protected ?Printer $printer = null;
|
||||
|
||||
/**
|
||||
* Find a log by its id
|
||||
*
|
||||
* @param Id $id
|
||||
* @param bool $includeContent
|
||||
* @return static|null
|
||||
*/
|
||||
public static function find(Id $id, bool $includeContent = true): ?static
|
||||
{
|
||||
$data = MongoDBClient::getInstance()->findLog($id, $includeContent);
|
||||
if ($data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::fromObject($id, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (string|Id)[] $ids
|
||||
* @param bool $includeContent
|
||||
* @return array<string, Log>
|
||||
*/
|
||||
public static function findAll(array $ids, bool $includeContent = true): array
|
||||
{
|
||||
$ids = array_map(fn($id) => (string)$id, $ids);
|
||||
$objects = MongoDBClient::getInstance()->findLogs($ids, $includeContent);
|
||||
$logs = [];
|
||||
foreach ($objects as $data) {
|
||||
$id = new Id($data->_id);
|
||||
$logs[$id->get()] = static::fromObject($id, $data);
|
||||
}
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Id $id
|
||||
* @param object $data
|
||||
* @return static
|
||||
*/
|
||||
protected static function fromObject(Id $id, object $data): static
|
||||
{
|
||||
return new static($id)
|
||||
->setContent($data->data ?? "")
|
||||
->setToken(isset($data->token) ? new Token($data->token) : null)
|
||||
->setMetadata(MetadataEntry::allFromArray($data->metadata ?? []))
|
||||
->setSource($data->source ?? null)
|
||||
->setCreated($data->created ?? null)
|
||||
->setExpires($data->expires ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and save a new log
|
||||
*
|
||||
* @param string $content
|
||||
* @param MetadataEntry[] $metadata
|
||||
* @param string|null $source
|
||||
* @return static
|
||||
*/
|
||||
public static function create(string $content, array $metadata = [], ?string $source = null): static
|
||||
{
|
||||
return new static()
|
||||
->setMetadata($metadata)
|
||||
->setSource($source)
|
||||
->setToken(new Token())
|
||||
->save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Id|null $id
|
||||
*/
|
||||
public function __construct(protected ?Id $id = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token|null $token
|
||||
* @return $this
|
||||
*/
|
||||
public function setToken(?Token $token): static
|
||||
{
|
||||
$this->token = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetadataEntry[] $metadata
|
||||
* @return $this
|
||||
*/
|
||||
public function setMetadata(array $metadata): static
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetadataEntry $metadataEntry
|
||||
* @return $this
|
||||
*/
|
||||
public function addMetadata(MetadataEntry $metadataEntry): static
|
||||
{
|
||||
$this->metadata[] = $metadataEntry;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $source
|
||||
* @return $this
|
||||
*/
|
||||
public function setSource(?string $source): static
|
||||
{
|
||||
if (is_string($source) && strlen($source) > static::SOURCE_MAX_LENGTH) {
|
||||
$source = substr($source, 0, static::SOURCE_MAX_LENGTH);
|
||||
}
|
||||
$this->source = $source;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSource(): ?string
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UTCDateTime|null $created
|
||||
* @return $this
|
||||
*/
|
||||
public function setCreated(?UTCDateTime $created): static
|
||||
{
|
||||
$this->created = $created;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UTCDateTime|null $expires
|
||||
* @return $this
|
||||
*/
|
||||
public function setExpires(?UTCDateTime $expires): static
|
||||
{
|
||||
$this->expires = $expires;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UTCDateTime|null
|
||||
*/
|
||||
public function getCreated(): ?UTCDateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UTCDateTime|null
|
||||
*/
|
||||
public function getExpires(): ?UTCDateTime
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @return $this
|
||||
*/
|
||||
public function setContent(string $content): static
|
||||
{
|
||||
$this->processAndDeobfuscate($content);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->log->getLogFile()->getContent();
|
||||
}
|
||||
|
||||
protected function processAndDeobfuscate(string $data): void
|
||||
{
|
||||
$this->process($data);
|
||||
$deobfuscator = new Deobfuscator($this->getCodexLog());
|
||||
if ($deobfuscatedData = $deobfuscator->deobfuscate()) {
|
||||
$this->process($deobfuscatedData);
|
||||
}
|
||||
}
|
||||
|
||||
protected function process($data): void
|
||||
{
|
||||
$this->log = new Detective()->setLogFile(new StringLogFile($data))->detect();
|
||||
$this->log->parse();
|
||||
if ($this->log instanceof AnalysableLogInterface) {
|
||||
$this->log->analyse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the codex log object
|
||||
*
|
||||
* @return LogInterface
|
||||
*/
|
||||
public function getCodexLog(): LogInterface
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the log analysis
|
||||
*
|
||||
* @return Analysis|null
|
||||
*/
|
||||
public function getAnalysis(): ?Analysis
|
||||
{
|
||||
$log = $this->getCodexLog();
|
||||
if ($log instanceof AnalysableLogInterface) {
|
||||
return $log->analyse();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Printer
|
||||
*/
|
||||
public function getPrinter(): Printer
|
||||
{
|
||||
if ($this->printer === null) {
|
||||
$this->printer = new Printer()->setLog($this->log)->setId($this->id);
|
||||
}
|
||||
return $this->printer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of lines in this log
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLinesCount(): int
|
||||
{
|
||||
$codexLog = $this->getCodexLog();
|
||||
$lines = 0;
|
||||
foreach ($codexLog as $entry) {
|
||||
$lines += count($entry);
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLinesString(): string
|
||||
{
|
||||
$lineCount = $this->getLinesCount();
|
||||
return $lineCount . ($lineCount === 1 ? " line" : " lines");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSize(): int
|
||||
{
|
||||
return strlen($this->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of error entries in the log
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getErrorsCount(): int
|
||||
{
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($this->log as $entry) {
|
||||
if ($entry->getLevel()->asInt() <= Level::ERROR->asInt()) {
|
||||
$errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return $errorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return $this->getErrorsCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorsString(): string
|
||||
{
|
||||
$errorCount = $this->getErrorsCount();
|
||||
return $errorCount . ($errorCount === 1 ? " error" : " errors");
|
||||
}
|
||||
|
||||
protected function generateId(): Id
|
||||
{
|
||||
do {
|
||||
$this->id = new Id();
|
||||
} while (MongoDBClient::getInstance()->hasLog($this->id));
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the log to the database
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function save(string $content): static
|
||||
{
|
||||
if ($this->id === null) {
|
||||
$this->generateId();
|
||||
}
|
||||
|
||||
$content = Filter::filterAll($content);
|
||||
|
||||
MongoDBClient::getInstance()->getLogsCollection()->insertOne([
|
||||
"_id" => $this->id->get(),
|
||||
"data" => $content,
|
||||
"token" => $this->token?->get(),
|
||||
"source" => $this->source,
|
||||
"metadata" => $this->metadata,
|
||||
"expires" => $this->expires = $this->getExpiryTimestamp(),
|
||||
"created" => $this->created = new UTCDateTime()
|
||||
]);
|
||||
|
||||
return $this->setContent($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UTCDateTime
|
||||
*/
|
||||
protected function getExpiryTimestamp(): UTCDateTime
|
||||
{
|
||||
$ttl = \Aternos\Mclogs\Config\Config::getInstance()->get(ConfigKey::STORAGE_TTL);
|
||||
$expires = time() + $ttl;
|
||||
return new UTCDateTime($expires * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew the expiry timestamp to expand the ttl
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function renew(): bool
|
||||
{
|
||||
$expires = $this->getExpiryTimestamp();
|
||||
$result = MongoDBClient::getInstance()->setLogExpires($this->id, $expires);
|
||||
if ($result) {
|
||||
$this->expires = $expires;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Uri
|
||||
*/
|
||||
public function getURL(): Uri
|
||||
{
|
||||
return URL::getBase()->withPath("/" . $this->id->get());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayURL(): string
|
||||
{
|
||||
$url = $this->getURL();
|
||||
return $url->getHost() . $url->getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Uri
|
||||
*/
|
||||
public function getRawURL(): Uri
|
||||
{
|
||||
return URL::getApi()->withPath("/1/raw/" . $this->id->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Id|null
|
||||
*/
|
||||
public function getId(): ?Id
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token|null
|
||||
*/
|
||||
public function getToken(): ?Token
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
return MongoDBClient::getInstance()->deleteLog($this->id->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetadataEntry[]
|
||||
*/
|
||||
public function getMetadata(): array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetadataEntry[]
|
||||
*/
|
||||
public function getVisibleMetadata(): array
|
||||
{
|
||||
return array_filter($this->metadata, function (MetadataEntry $entry) {
|
||||
return $entry->isVisible();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function setTokenCookie(): bool
|
||||
{
|
||||
if (!$this->getToken()) {
|
||||
return false;
|
||||
}
|
||||
return new TokenCookie($this)->set($this->getToken()->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasValidTokenCookie(): bool
|
||||
{
|
||||
$tokenCookie = new TokenCookie();
|
||||
$cookieValue = $tokenCookie->getValue();
|
||||
if ($cookieValue === null || !$this->getToken()) {
|
||||
return false;
|
||||
}
|
||||
return $this->getToken()->matches($cookieValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
return $this->getCodexLog()?->getTitle() . " [#" . $this->getId()?->get() . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPageDescription(): string
|
||||
{
|
||||
$description = $this->getLinesString();
|
||||
if ($this->hasErrors()) {
|
||||
$description .= " | " . $this->getErrorsString();
|
||||
}
|
||||
|
||||
$problems = $this->getAnalysis()->getProblems();
|
||||
|
||||
if (count($problems) > 0) {
|
||||
$problemString = "problems";
|
||||
if (count($problems) === 1) {
|
||||
$problemString = "problem";
|
||||
}
|
||||
$description .= " | " . count($problems) . " " . $problemString . " automatically detected";
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
}
|
||||
20
src/Printer/FormatModification.php
Normal file
20
src/Printer/FormatModification.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Printer;
|
||||
|
||||
/**
|
||||
* Class FormatModification
|
||||
*
|
||||
* @package Printer
|
||||
*/
|
||||
class FormatModification extends \Aternos\Codex\Minecraft\Printer\FormatModification
|
||||
{
|
||||
/**
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
protected function getClasses(string $format): string
|
||||
{
|
||||
return "format format-" . $format;
|
||||
}
|
||||
}
|
||||
88
src/Printer/Printer.php
Normal file
88
src/Printer/Printer.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Printer;
|
||||
|
||||
use Aternos\Codex\Log\Entry;
|
||||
use Aternos\Codex\Log\EntryInterface;
|
||||
use Aternos\Codex\Log\Level;
|
||||
use Aternos\Codex\Log\LineInterface;
|
||||
use Aternos\Codex\Printer\ModifiableDefaultPrinter;
|
||||
use Aternos\Mclogs\Id;
|
||||
|
||||
/**
|
||||
* Class Printer
|
||||
*
|
||||
* @package Printer
|
||||
*/
|
||||
class Printer extends ModifiableDefaultPrinter
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addModification(new FormatModification());
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Id
|
||||
*/
|
||||
protected Id $id;
|
||||
|
||||
/**
|
||||
* @param Id $id
|
||||
* @return Printer
|
||||
*/
|
||||
public function setId(Id $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function printLog(): string
|
||||
{
|
||||
return '<div class="log-inner">' . parent::printLog() . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EntryInterface|null $entry
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function printEntry(?EntryInterface $entry = null): string
|
||||
{
|
||||
$entry = $entry ?? $this->entry;
|
||||
/** @var Entry $entry */
|
||||
$return = '';
|
||||
$first = true;
|
||||
foreach ($entry as $line) {
|
||||
$entryClass = "entry-no-error";
|
||||
if ($entry->getLevel()->asInt() <= Level::ERROR->asInt()) {
|
||||
$entryClass = "entry-error";
|
||||
}
|
||||
$return .= '<div class="entry ' . $entryClass . '">';
|
||||
$return .= '<div class="line-number-container"><a href="/' . $this->id->get() . '#L' . $line->getNumber() . '" id="L' . $line->getNumber() . '" class="line-number">' . $line->getNumber() . '</a></div>';
|
||||
$return .= '<div class="line-content"><span class="level level-' . $entry->getLevel()->asString() . ((!$first) ? " multiline" : "") . '">';
|
||||
$lineString = $this->printLine($line);
|
||||
if ($entry->getPrefix() !== null) {
|
||||
$prefix = htmlentities($entry->getPrefix());
|
||||
$lineString = str_replace($prefix, '<span class="level-prefix">' . $prefix . '</span>', $lineString);
|
||||
}
|
||||
$return .= $lineString;
|
||||
$return .= '</span></div>';
|
||||
$return .= '</div>';
|
||||
$first = false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LineInterface $line
|
||||
* @return string
|
||||
*/
|
||||
protected function printLine(LineInterface $line): string
|
||||
{
|
||||
return $this->runModifications(htmlentities($line->getText())) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
8
src/Router/Action.php
Normal file
8
src/Router/Action.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Router;
|
||||
|
||||
abstract class Action
|
||||
{
|
||||
abstract public function run(): bool;
|
||||
}
|
||||
17
src/Router/Method.php
Normal file
17
src/Router/Method.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Router;
|
||||
|
||||
enum Method: string
|
||||
{
|
||||
case GET = 'GET';
|
||||
case POST = 'POST';
|
||||
case PUT = 'PUT';
|
||||
case DELETE = 'DELETE';
|
||||
case OPTIONS = 'OPTIONS';
|
||||
|
||||
public static function getCurrent(): self
|
||||
{
|
||||
return self::tryFrom($_SERVER['REQUEST_METHOD']) ?? self::GET;
|
||||
}
|
||||
}
|
||||
51
src/Router/Route.php
Normal file
51
src/Router/Route.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Router;
|
||||
|
||||
class Route
|
||||
{
|
||||
public function __construct(
|
||||
protected Method $method,
|
||||
protected string $pattern,
|
||||
protected Action $action
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Method $method
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
public function matches(Method $method, string $path): bool
|
||||
{
|
||||
if ($this->getMethod() !== $method) {
|
||||
return false;
|
||||
}
|
||||
return preg_match($this->getPattern(), $path) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Method
|
||||
*/
|
||||
public function getMethod(): Method
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPattern(): string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Action
|
||||
*/
|
||||
public function getAction(): Action
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
}
|
||||
73
src/Router/Router.php
Normal file
73
src/Router/Router.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Router;
|
||||
|
||||
use Aternos\Mclogs\Util\Singleton;
|
||||
use Aternos\Mclogs\Util\URL;
|
||||
|
||||
class Router
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* @var Route[]
|
||||
*/
|
||||
protected array $routes = [];
|
||||
|
||||
protected ?Action $defaultAction = null;
|
||||
|
||||
/**
|
||||
* @param Method $method
|
||||
* @param string $pattern
|
||||
* @param Action $action
|
||||
* @return $this
|
||||
*/
|
||||
public function register(Method $method, string $pattern, Action $action): static
|
||||
{
|
||||
$this->routes[] = new Route($method, $pattern, $action);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Action $defaultAction
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefaultAction(Action $defaultAction): static
|
||||
{
|
||||
$this->defaultAction = $defaultAction;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function run(): static
|
||||
{
|
||||
$route = $this->findRoute();
|
||||
if (!$route) {
|
||||
$this->defaultAction?->run();
|
||||
return $this;
|
||||
}
|
||||
$result = $route->getAction()->run();
|
||||
if (!$result) {
|
||||
$this->defaultAction?->run();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Route|null
|
||||
*/
|
||||
protected function findRoute(): ?Route
|
||||
{
|
||||
$path = URL::getCurrent()->getPath();
|
||||
$method = Method::getCurrent();
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route->matches($method, $path)) {
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
234
src/Storage/MongoDBClient.php
Normal file
234
src/Storage/MongoDBClient.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Storage;
|
||||
|
||||
use Aternos\Mclogs\Config\Config;
|
||||
use Aternos\Mclogs\Config\ConfigKey;
|
||||
use Aternos\Mclogs\Util\Singleton;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
use MongoDB\Client;
|
||||
use MongoDB\Collection;
|
||||
use MongoDB\Database;
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
class MongoDBClient
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
protected ?Client $connection = null;
|
||||
protected Database $database;
|
||||
|
||||
protected ?Collection $logs = null;
|
||||
protected ?Collection $cache = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getConnectionURL(): string
|
||||
{
|
||||
$configUrl = Config::getInstance()->get(ConfigKey::MONGODB_URL);
|
||||
$url = new Uri($configUrl);
|
||||
$query = $url->getQuery();
|
||||
$queryParams = [];
|
||||
if ($query !== null) {
|
||||
parse_str($query, $queryParams);
|
||||
}
|
||||
if (!isset($queryParams['serverSelectionTimeoutMS'])) {
|
||||
$queryParams['serverSelectionTimeoutMS'] = 5_000;
|
||||
}
|
||||
if (!isset($queryParams['socketTimeoutMS'])) {
|
||||
$queryParams['socketTimeoutMS'] = 60_000;
|
||||
}
|
||||
$newQuery = http_build_query($queryParams);
|
||||
$newUrl = $url->withQuery($newQuery);
|
||||
return $newUrl->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MongoDB
|
||||
*/
|
||||
protected function connect(): void
|
||||
{
|
||||
if ($this->connection === null) {
|
||||
$config = Config::getInstance();
|
||||
$this->connection = new Client($this->getConnectionURL());
|
||||
$this->database = $this->connection->getDatabase($config->get(ConfigKey::MONGODB_DATABASE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure indexes exist
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ensureIndexes(): void
|
||||
{
|
||||
$logs = $this->getLogsCollection();
|
||||
$logs->createIndex(['expires' => 1], ['expireAfterSeconds' => 0]);
|
||||
|
||||
$cache = $this->getCacheCollection();
|
||||
$cache->createIndex(['expires' => 1], ['expireAfterSeconds' => 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
$this->connection = null;
|
||||
$this->logs = null;
|
||||
$this->cache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection for logs
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getLogsCollection(): Collection
|
||||
{
|
||||
if ($this->logs === null) {
|
||||
$this->connect();
|
||||
$this->logs = $this->database->getCollection('logs');
|
||||
}
|
||||
return $this->logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param bool $includeContent
|
||||
* @return object|null
|
||||
*/
|
||||
public function findLog(string $id, bool $includeContent = true): ?object
|
||||
{
|
||||
$options = [];
|
||||
if (!$includeContent) {
|
||||
$options['projection'] = ['data' => 0];
|
||||
}
|
||||
|
||||
$collection = $this->getLogsCollection();
|
||||
$result = $collection->findOne(['_id' => $id], $options);
|
||||
if ($result === null) {
|
||||
// Check for legacy ID without the first character
|
||||
return $collection->findOne(['_id' => substr($id, 1)], $options);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $ids
|
||||
* @param bool $includeContent
|
||||
* @return object[]
|
||||
*/
|
||||
public function findLogs(array $ids, bool $includeContent = true): array
|
||||
{
|
||||
$options = [];
|
||||
if (!$includeContent) {
|
||||
$options['projection'] = ['data' => 0];
|
||||
}
|
||||
|
||||
$collection = $this->getLogsCollection();
|
||||
$results = $collection->find(['_id' => ['$in' => $ids]], $options)->toArray();
|
||||
$foundIds = [];
|
||||
foreach ($results as $result) {
|
||||
$foundIds[] = (string)$result->_id;
|
||||
}
|
||||
|
||||
$missingIds = array_diff($ids, $foundIds);
|
||||
if (!empty($missingIds)) {
|
||||
$legacyIds = [];
|
||||
foreach ($missingIds as $id) {
|
||||
$legacyIds[substr($id, 1)] = $id;
|
||||
}
|
||||
|
||||
// Check for legacy IDs without the first character
|
||||
$legacyResults = $collection->find(['_id' => ['$in' => array_keys($legacyIds)]], $options)->toArray();
|
||||
foreach ($legacyResults as $result) {
|
||||
// Map the legacy ID back to the original ID
|
||||
$originalId = $legacyIds[(string)$result->_id];
|
||||
$result->_id = $originalId;
|
||||
|
||||
// Add the found legacy results to the main results array
|
||||
$results[] = $result;
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteLog(string $id): bool
|
||||
{
|
||||
$collection = $this->getLogsCollection();
|
||||
$result = $collection->deleteOne(['_id' => $id]);
|
||||
if ($result->getDeletedCount() === 0) {
|
||||
// Check for legacy ID without the first character
|
||||
$result = $collection->deleteOne(['_id' => substr($id, 1)]);
|
||||
return $result->getDeletedCount() === 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ids
|
||||
* @return int Number of logs deleted
|
||||
*/
|
||||
public function deleteLogs(array $ids): int
|
||||
{
|
||||
$collection = $this->getLogsCollection();
|
||||
$result = $collection->deleteMany(['_id' => ['$in' => $ids]]);
|
||||
$deletedCount = $result->getDeletedCount();
|
||||
|
||||
if ($deletedCount === count($ids)) {
|
||||
return $deletedCount;
|
||||
}
|
||||
|
||||
// Check for legacy IDs without the first character
|
||||
$legacyIds = [];
|
||||
foreach ($ids as $id) {
|
||||
$legacyIds[] = substr($id, 1);
|
||||
}
|
||||
$legacyResult = $collection->deleteMany(['_id' => ['$in' => $legacyIds]]);
|
||||
return $deletedCount + $legacyResult->getDeletedCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public function hasLog(string $id): bool
|
||||
{
|
||||
return $this->findLog($id) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param UTCDateTime $expires
|
||||
* @return bool
|
||||
*/
|
||||
public function setLogExpires(string $id, UTCDateTime $expires): bool
|
||||
{
|
||||
$collection = $this->getLogsCollection();
|
||||
$result = $collection->updateOne(
|
||||
['_id' => $id],
|
||||
['$set' => ['expires' => $expires]]
|
||||
);
|
||||
return $result->getModifiedCount() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection for caching
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getCacheCollection(): Collection
|
||||
{
|
||||
if ($this->cache === null) {
|
||||
$this->connect();
|
||||
$this->cache = $this->database->getCollection('cache');
|
||||
}
|
||||
return $this->cache;
|
||||
}
|
||||
}
|
||||
37
src/Util/Singleton.php
Normal file
37
src/Util/Singleton.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Util;
|
||||
|
||||
trait Singleton
|
||||
{
|
||||
/**
|
||||
* @var static[]
|
||||
*/
|
||||
protected static array $instances = [];
|
||||
|
||||
public static function getInstance(): static
|
||||
{
|
||||
$class = get_called_class();
|
||||
|
||||
if (!isset(static::$instances[$class])) {
|
||||
static::$instances[$class] = new static;
|
||||
}
|
||||
|
||||
return static::$instances[$class];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prohibited for singleton
|
||||
*/
|
||||
protected function __clone()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Prohibited for singleton
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
53
src/Util/TimeInterval.php
Normal file
53
src/Util/TimeInterval.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Util;
|
||||
|
||||
class TimeInterval
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
protected const array UNITS = [
|
||||
"year" => 365 * 24 * 60 * 60,
|
||||
"month" => 30 * 24 * 60 * 60,
|
||||
"week" => 7 * 24 * 60 * 60,
|
||||
"day" => 24 * 60 * 60,
|
||||
"hour" => 60 * 60,
|
||||
"minute" => 60,
|
||||
"second" => 1,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param int $value
|
||||
* @param string $unit
|
||||
* @return string
|
||||
*/
|
||||
protected function formatUnit(int $value, string $unit): string
|
||||
{
|
||||
if ($value === 1) {
|
||||
return $value . " " . $unit;
|
||||
} else {
|
||||
return $value . " " . $unit . "s";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $duration
|
||||
* @param string $separator
|
||||
* @return string
|
||||
*/
|
||||
public function format(int $duration, string $separator = ", "): string
|
||||
{
|
||||
$parts = [];
|
||||
while ($duration > 0) {
|
||||
foreach (self::UNITS as $unit => $seconds) {
|
||||
if ($duration >= $seconds) {
|
||||
$value = intdiv($duration, $seconds);
|
||||
$duration -= $value * $seconds;
|
||||
$parts[] = $this->formatUnit($value, $unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode($separator, $parts);
|
||||
}
|
||||
}
|
||||
128
src/Util/URL.php
Normal file
128
src/Util/URL.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Aternos\Mclogs\Util;
|
||||
|
||||
use Uri\Rfc3986\Uri;
|
||||
|
||||
class URL
|
||||
{
|
||||
protected const string API_SUBDOMAIN = "api.";
|
||||
|
||||
protected static ?Uri $base = null;
|
||||
protected static ?Uri $api = null;
|
||||
protected static ?Uri $current = null;
|
||||
|
||||
public static function clear(): void
|
||||
{
|
||||
static::$base = null;
|
||||
static::$api = null;
|
||||
static::$current = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected static function readProtocol(): string
|
||||
{
|
||||
if (isset($_SERVER['HTTP_FORWARDED'])) {
|
||||
$forwarded = explode(';', $_SERVER['HTTP_FORWARDED']);
|
||||
foreach ($forwarded as $part) {
|
||||
$part = trim($part);
|
||||
$partParts = explode('=', $part, 2);
|
||||
if (count($partParts) === 2 && strtolower($partParts[0]) === 'proto') {
|
||||
$protocol = $partParts[1];
|
||||
$protocol = trim($protocol, '"\'');
|
||||
return strtolower($protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||
$protoParts = explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO']);
|
||||
return strtolower(trim($protoParts[0]));
|
||||
}
|
||||
if (isset($_SERVER['REQUEST_SCHEME'])) {
|
||||
return strtolower($_SERVER['REQUEST_SCHEME']);
|
||||
}
|
||||
return 'http';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected static function getProtocol(): string
|
||||
{
|
||||
$protocol = static::readProtocol();
|
||||
if ($protocol === 'https') {
|
||||
return 'https';
|
||||
}
|
||||
return 'http';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base URL
|
||||
*
|
||||
* @return Uri
|
||||
*/
|
||||
public static function getBase(): Uri
|
||||
{
|
||||
if (static::$base) {
|
||||
return static::$base;
|
||||
}
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
if (str_starts_with($host, static::API_SUBDOMAIN)) {
|
||||
$host = substr($host, strlen(static::API_SUBDOMAIN));
|
||||
}
|
||||
return static::$base = new Uri(static::getProtocol() . "://" . $host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API URL
|
||||
*
|
||||
* @return Uri
|
||||
*/
|
||||
public static function getApi(): Uri
|
||||
{
|
||||
if (static::$api) {
|
||||
return static::$api;
|
||||
}
|
||||
$base = static::getBase();
|
||||
return static::$api = $base->withHost(static::API_SUBDOMAIN . $base->getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Uri
|
||||
*/
|
||||
public static function getCurrent(): Uri
|
||||
{
|
||||
if (static::$current) {
|
||||
return static::$current;
|
||||
}
|
||||
$scheme = $_SERVER['REQUEST_SCHEME'];
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
return static::$current = new Uri("$scheme://$host$requestUri");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function isApi(): bool
|
||||
{
|
||||
$currentHost = static::getCurrent()->getHost();
|
||||
$apiHost = static::getApi()->getHost();
|
||||
return $currentHost === $apiHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function getLastPathPart(): string
|
||||
{
|
||||
$path = static::getCurrent()->getPath();
|
||||
$parts = explode("/", $path);
|
||||
do {
|
||||
$part = trim(array_pop($parts));
|
||||
} while ($part === "" && count($parts) > 0);
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user