584 lines
16 KiB
PHP
584 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Libs\Config;
|
|
use App\Libs\Container;
|
|
use App\Libs\Entity\StateInterface;
|
|
use App\Libs\Extends\Date;
|
|
use App\Libs\Servers\ServerInterface;
|
|
use Nyholm\Psr7\Response;
|
|
use Nyholm\Psr7\Uri;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\SimpleCache\CacheInterface;
|
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
|
|
|
if (!function_exists('env')) {
|
|
function env(string $key, mixed $default = null): mixed
|
|
{
|
|
if (false === ($value = $_ENV[$key] ?? getenv($key))) {
|
|
return getValue($default);
|
|
}
|
|
|
|
return match (strtolower($value)) {
|
|
'true', '(true)' => true,
|
|
'false', '(false)' => false,
|
|
'empty', '(empty)' => '',
|
|
'null', '(null)' => null,
|
|
default => $value,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!function_exists('getValue')) {
|
|
function getValue(mixed $var): mixed
|
|
{
|
|
return ($var instanceof Closure) ? $var() : $var;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('makeDate')) {
|
|
/**
|
|
* Make Date Time Object.
|
|
*
|
|
* @param string|int $date Defaults to now
|
|
* @param string|DateTimeZone|null $tz For given $date, not for display.
|
|
*
|
|
* @return Date
|
|
*/
|
|
function makeDate(string|int $date = 'now', DateTimeZone|string|null $tz = null): Date
|
|
{
|
|
if (ctype_digit((string)$date)) {
|
|
$date = '@' . $date;
|
|
}
|
|
|
|
if (null === $tz) {
|
|
$tz = date_default_timezone_get();
|
|
}
|
|
|
|
if (!($tz instanceof DateTimeZone)) {
|
|
$tz = new DateTimeZone($tz);
|
|
}
|
|
|
|
return (new Date($date))->setTimezone($tz);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('ag')) {
|
|
function ag(array|object $array, string|array|null $path, mixed $default = null, string $separator = '.'): mixed
|
|
{
|
|
if (empty($path)) {
|
|
return $array;
|
|
}
|
|
|
|
if (!is_array($array)) {
|
|
$array = get_object_vars($array);
|
|
}
|
|
|
|
if (is_array($path)) {
|
|
foreach ($path as $key) {
|
|
$val = ag($array, $key, '_not_set');
|
|
if ('_not_set' === $val) {
|
|
continue;
|
|
}
|
|
return $val;
|
|
}
|
|
return getValue($default);
|
|
}
|
|
|
|
if (array_key_exists($path, $array)) {
|
|
return $array[$path];
|
|
}
|
|
|
|
if (!str_contains($path, $separator)) {
|
|
return $array[$path] ?? getValue($default);
|
|
}
|
|
|
|
foreach (explode($separator, $path) as $segment) {
|
|
if (is_array($array) && array_key_exists($segment, $array)) {
|
|
$array = $array[$segment];
|
|
} else {
|
|
return getValue($default);
|
|
}
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('ag_set')) {
|
|
/**
|
|
* Set an array item to a given value using "dot" notation.
|
|
*
|
|
* If no key is given to the method, the entire array will be replaced.
|
|
*
|
|
* @param array $array
|
|
* @param string $path
|
|
* @param mixed $value
|
|
* @param string $separator
|
|
*
|
|
* @return array return modified array.
|
|
*/
|
|
function ag_set(array $array, string $path, mixed $value, string $separator = '.'): array
|
|
{
|
|
$keys = explode($separator, $path);
|
|
|
|
$at = &$array;
|
|
|
|
while (count($keys) > 0) {
|
|
if (1 === count($keys)) {
|
|
if (is_array($at)) {
|
|
$at[array_shift($keys)] = $value;
|
|
} else {
|
|
throw new RuntimeException("Can not set value at this path ($path) because its not array.");
|
|
}
|
|
} else {
|
|
$path = array_shift($keys);
|
|
if (!isset($at[$path])) {
|
|
$at[$path] = [];
|
|
}
|
|
$at = &$at[$path];
|
|
}
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('ag_exists')) {
|
|
/**
|
|
* Determine if the given key exists in the provided array.
|
|
*
|
|
* @param array $array
|
|
* @param string|int $path
|
|
* @param string $separator
|
|
*
|
|
* @return bool
|
|
*/
|
|
function ag_exists(array $array, string|int $path, string $separator = '.'): bool
|
|
{
|
|
if (is_int($path)) {
|
|
return isset($array[$path]);
|
|
}
|
|
|
|
foreach (explode($separator, $path) as $lookup) {
|
|
if (isset($array[$lookup])) {
|
|
$array = $array[$lookup];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('ag_delete')) {
|
|
/**
|
|
* Delete given key path.
|
|
*
|
|
* @param array $array
|
|
* @param int|string $path
|
|
* @param string $separator
|
|
* @return array
|
|
*/
|
|
function ag_delete(array $array, string|int $path, string $separator = '.'): array
|
|
{
|
|
if (array_key_exists($path, $array)) {
|
|
unset($array[$path]);
|
|
|
|
return $array;
|
|
}
|
|
|
|
if (is_int($path)) {
|
|
if (isset($array[$path])) {
|
|
unset($array[$path]);
|
|
}
|
|
return $array;
|
|
}
|
|
|
|
$items = &$array;
|
|
|
|
$segments = explode($separator, $path);
|
|
|
|
$lastSegment = array_pop($segments);
|
|
|
|
foreach ($segments as $segment) {
|
|
if (!isset($items[$segment]) || !is_array($items[$segment])) {
|
|
continue;
|
|
}
|
|
|
|
$items = &$items[$segment];
|
|
}
|
|
|
|
if (null !== $lastSegment && array_key_exists($lastSegment, $items)) {
|
|
unset($items[$lastSegment]);
|
|
}
|
|
|
|
return $array;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('fixPath')) {
|
|
function fixPath(string $path): string
|
|
{
|
|
return rtrim(implode(DIRECTORY_SEPARATOR, explode(DIRECTORY_SEPARATOR, $path)), DIRECTORY_SEPARATOR);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('fsize')) {
|
|
function fsize(string|int $bytes = 0, bool $showUnit = true, int $decimals = 2, int $mod = 1000): string
|
|
{
|
|
$sz = 'BKMGTP';
|
|
|
|
$factor = floor((strlen((string)$bytes) - 1) / 3);
|
|
|
|
return sprintf("%.{$decimals}f", (int)($bytes) / ($mod ** $factor)) . ($showUnit ? $sz[(int)$factor] : '');
|
|
}
|
|
}
|
|
|
|
if (!function_exists('saveWebhookPayload')) {
|
|
function saveWebhookPayload(StateInterface $entity, ServerRequestInterface $request): void
|
|
{
|
|
$content = [
|
|
'request' => [
|
|
'server' => $request->getServerParams(),
|
|
'body' => (string)$request->getBody(),
|
|
'query' => $request->getQueryParams(),
|
|
],
|
|
'parsed' => $request->getParsedBody(),
|
|
'attributes' => $request->getAttributes(),
|
|
'entity' => $entity->getAll(),
|
|
];
|
|
|
|
@file_put_contents(
|
|
Config::get('tmpDir') . '/webhooks/' . sprintf(
|
|
'webhook.%s.%s.%s.json',
|
|
$entity->via,
|
|
ag($entity->getExtra($entity->via), 'event', 'unknown'),
|
|
ag($request->getServerParams(), 'X_REQUEST_ID', time())
|
|
),
|
|
json_encode(value: $content, flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_IGNORE)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('saveRequestPayload')) {
|
|
function saveRequestPayload(ServerRequestInterface $request): void
|
|
{
|
|
$content = [
|
|
'query' => $request->getQueryParams(),
|
|
'parsed' => $request->getParsedBody(),
|
|
'server' => $request->getServerParams(),
|
|
'body' => (string)$request->getBody(),
|
|
'attributes' => $request->getAttributes(),
|
|
];
|
|
|
|
@file_put_contents(
|
|
Config::get('tmpDir') . '/debug/' . sprintf(
|
|
'request.%s.json',
|
|
ag($request->getServerParams(), 'X_REQUEST_ID', (string)time())
|
|
),
|
|
json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('jsonResponse')) {
|
|
function jsonResponse(int $status, array $body, $headers = []): ResponseInterface
|
|
{
|
|
$headers['Content-Type'] = 'application/json';
|
|
|
|
return new Response(
|
|
status: $status,
|
|
headers: $headers,
|
|
body: json_encode(
|
|
$body,
|
|
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('httpClientChunks')) {
|
|
/**
|
|
* Handle Response Stream as Chunks
|
|
*
|
|
* @param ResponseStreamInterface $responseStream
|
|
* @return Generator
|
|
*
|
|
* @throws TransportExceptionInterface
|
|
*/
|
|
function httpClientChunks(ResponseStreamInterface $responseStream): Generator
|
|
{
|
|
foreach ($responseStream as $chunk) {
|
|
yield $chunk->getContent();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!function_exists('queuePush')) {
|
|
function queuePush(StateInterface $entity): void
|
|
{
|
|
if (!$entity->hasGuids() && !$entity->hasRelativeGuid()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$cache = Container::get(CacheInterface::class);
|
|
|
|
$list = $cache->get('queue', []);
|
|
|
|
$list[$entity->id] = ['id' => $entity->id];
|
|
|
|
$cache->set('queue', $list, new DateInterval('P7D'));
|
|
} catch (\Psr\SimpleCache\InvalidArgumentException $e) {
|
|
Container::get(LoggerInterface::class)->error($e->getMessage(), $e->getTrace());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!function_exists('afterLast')) {
|
|
function afterLast(string $subject, string $search): string
|
|
{
|
|
if (empty($search)) {
|
|
return $subject;
|
|
}
|
|
|
|
$position = mb_strrpos($subject, $search, 0);
|
|
|
|
if (false === $position) {
|
|
return $subject;
|
|
}
|
|
|
|
return mb_substr($subject, $position + mb_strlen($search));
|
|
}
|
|
}
|
|
|
|
if (!function_exists('before')) {
|
|
function before(string $subject, string $search): string
|
|
{
|
|
return $search === '' ? $subject : explode($search, $subject)[0];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('after')) {
|
|
function after(string $subject, string $search): string
|
|
{
|
|
return empty($search) ? $subject : array_reverse(explode($search, $subject, 2))[0];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('makeServer')) {
|
|
/**
|
|
* @param array{name:string|null, type:string, url:string, token:string|int|null, user:string|int|null, persist:array, options:array} $server
|
|
* @param string|null $name server name.
|
|
* @return ServerInterface
|
|
*
|
|
* @throws RuntimeException if configuration is wrong.
|
|
*/
|
|
function makeServer(array $server, string|null $name = null): ServerInterface
|
|
{
|
|
if (null === ($serverType = ag($server, 'type'))) {
|
|
throw new RuntimeException('No server type was selected.');
|
|
}
|
|
|
|
if (null === ag($server, 'url')) {
|
|
throw new RuntimeException('No url was set for server.');
|
|
}
|
|
|
|
if (null === ($class = Config::get("supported.{$serverType}", null))) {
|
|
throw new RuntimeException(
|
|
sprintf(
|
|
'Unexpected server type was given. Was expecting [%s] but got \'%s\' instead.',
|
|
$serverType,
|
|
implode('|', Config::get("supported", []))
|
|
)
|
|
);
|
|
}
|
|
|
|
return Container::getNew($class)->setUp(
|
|
name: $name ?? ag($server, 'name', fn() => md5(ag($server, 'url'))),
|
|
url: new Uri(ag($server, 'url')),
|
|
token: ag($server, 'token', null),
|
|
userId: ag($server, 'user', null),
|
|
uuid: ag($server, 'uuid', null),
|
|
persist: ag($server, 'persist', []),
|
|
options: ag($server, 'options', []),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('arrayToString')) {
|
|
function arrayToString(array $arr, string $separator = ', '): string
|
|
{
|
|
$list = [];
|
|
|
|
foreach ($arr as $key => $val) {
|
|
if (is_object($val)) {
|
|
if (($val instanceof JsonSerializable)) {
|
|
$val = json_encode($val, flags: JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
} elseif (($val instanceof Stringable) || method_exists($val, '__toString')) {
|
|
$val = (string)$val;
|
|
} else {
|
|
$val = get_object_vars($val);
|
|
}
|
|
}
|
|
|
|
if (is_array($val)) {
|
|
$val = '[ ' . arrayToString($val) . ' ]';
|
|
} else {
|
|
$val = $val ?? 'None';
|
|
}
|
|
|
|
$list[] = sprintf("(%s: %s)", $key, $val);
|
|
}
|
|
|
|
return implode($separator, $list);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('commandContext')) {
|
|
function commandContext(): string
|
|
{
|
|
if (env('IN_DOCKER')) {
|
|
return sprintf('docker exec -ti %s console ', env('CONTAINER_NAME', 'watchstate'));
|
|
}
|
|
|
|
return ($_SERVER['argv'][0] ?? 'php console') . ' ';
|
|
}
|
|
}
|
|
|
|
if (!function_exists('getAppVersion')) {
|
|
function getAppVersion(): string
|
|
{
|
|
$version = Config::get('version', 'dev-master');
|
|
|
|
if ('$(version_via_ci)' === $version) {
|
|
$gitDir = ROOT_PATH . '/.git/';
|
|
|
|
if (is_dir($gitDir)) {
|
|
$cmd = 'git --git-dir=%1$s describe --exact-match --tags 2> /dev/null || git --git-dir=%1$s rev-parse --short HEAD';
|
|
exec(sprintf($cmd, escapeshellarg($gitDir)), $output, $status);
|
|
|
|
if (0 === $status) {
|
|
return $output[0] ?? 'dev-master';
|
|
}
|
|
}
|
|
|
|
return 'dev-master';
|
|
}
|
|
|
|
return $version;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('t')) {
|
|
function t($phrase, string|int ...$args): string
|
|
{
|
|
static $lang;
|
|
|
|
if (null === $lang) {
|
|
$lang = require __DIR__ . '/../../config/lang.php';
|
|
}
|
|
|
|
if (isset($lang[$phrase])) {
|
|
throw new InvalidArgumentException(
|
|
sprintf('Invalid language definition \'%s\' key was given.', $phrase)
|
|
);
|
|
}
|
|
|
|
$text = $lang[$phrase];
|
|
|
|
if (!empty($args)) {
|
|
$text = sprintf($text, ...$args);
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
}
|
|
|
|
|
|
if (!function_exists('isValidName')) {
|
|
/**
|
|
* Allow only [Aa-Zz][0-9][_] in server names.
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return bool
|
|
*/
|
|
function isValidName(string $name): bool
|
|
{
|
|
return 1 === preg_match('/^\w+$/', $name);
|
|
}
|
|
}
|
|
|
|
if (false === function_exists('filterResponse')) {
|
|
function filterResponse(object|array $item, array $cast = []): array
|
|
{
|
|
if (false === is_array($item)) {
|
|
$item = (array)$item;
|
|
}
|
|
|
|
if (empty($cast)) {
|
|
return $item;
|
|
}
|
|
|
|
$modified = [];
|
|
|
|
foreach ($item as $key => $value) {
|
|
if (true === is_array($value) || true === is_object($value)) {
|
|
$modified[$key] = filterResponse($value, $cast);
|
|
continue;
|
|
}
|
|
|
|
if (null === ($cast[$key] ?? null)) {
|
|
$modified[$key] = $value;
|
|
continue;
|
|
}
|
|
|
|
$modified[$key] = match ($cast[$key] ?? null) {
|
|
'datetime' => makeDate($value),
|
|
'size' => strlen((string)$value) >= 4 ? fsize($value) : $value,
|
|
'duration_sec' => formatDuration($value),
|
|
'duration_mil' => formatDuration($value / 10000),
|
|
'bool' => (bool)$value,
|
|
default => is_callable($cast[$key] ?? null) ? $cast[$key]($value) : $value,
|
|
};
|
|
}
|
|
|
|
return $modified;
|
|
}
|
|
}
|
|
|
|
if (false === function_exists('formatDuration')) {
|
|
function formatDuration(int|float $milliseconds): string
|
|
{
|
|
$seconds = floor($milliseconds / 1000);
|
|
$minutes = floor($seconds / 60);
|
|
$hours = floor($minutes / 60);
|
|
$seconds %= 60;
|
|
$minutes %= 60;
|
|
|
|
return sprintf('%02u:%02u:%02u', $hours, $minutes, $seconds);
|
|
}
|
|
}
|
|
|
|
if (false === function_exists('array_keys_diff')) {
|
|
/**
|
|
* Return keys that match or does not match keys in list.
|
|
*
|
|
* @param array $base array containing all keys.
|
|
* @param array $list list of keys that you want to filter based on.
|
|
* @param bool $has Whether to get keys that exist in $list or exclude them.
|
|
* @return array
|
|
*/
|
|
function array_keys_diff(array $base, array $list, bool $has = true): array
|
|
{
|
|
return array_filter($base, fn($key) => $has === in_array($key, $list), ARRAY_FILTER_USE_KEY);
|
|
}
|
|
}
|