first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Helpers;
use App\Models\Task;
use App\Models\TaskComment;
use Illuminate\Http\UploadedFile;
use Str;
class AttachmentHelper
{
protected const TYPE_PLACEHOLDER = '__ABLE_TYPE__';
protected const MAX_FILE_NAME_LENGTH = 255; // name.extension
public const ABLE_BY = [
Task::TYPE => Task::class,
TaskComment::TYPE => TaskComment::class
];
public static function isAble(string $able_type): bool
{
return array_key_exists($able_type, self::ABLE_BY);
}
public static function getAbles(): array
{
return array_keys(self::ABLE_BY);
}
public static function getFileName(UploadedFile $file): string
{
$fileName = $file->getClientOriginalName();
$extension = ".{$file->clientExtension()}";
$maxNameLength = (self::MAX_FILE_NAME_LENGTH - Str::length($extension));
if (Str::length($fileName) > $maxNameLength || Str::endsWith($fileName, $extension) === false){
$fileName = Str::substr($fileName, 0, $maxNameLength) . $extension;
}
return $fileName;
}
public static function getEvents($parentCreated, $parentUpdated, $parentDeleted): array
{
return self::getMappedEvents([
'event.after.action.' . self::TYPE_PLACEHOLDER . '.create' => $parentCreated,
'event.after.action.' . self::TYPE_PLACEHOLDER . '.edit' => $parentUpdated,
'event.after.action.' . self::TYPE_PLACEHOLDER . '.destroy' => $parentDeleted,
]);
}
public static function getFilters(array $addRequestRulesForParent): array
{
return self::getMappedEvents([
'filter.validation.' . self::TYPE_PLACEHOLDER . '.edit' => $addRequestRulesForParent,
'filter.validation.' . self::TYPE_PLACEHOLDER . '.create' => $addRequestRulesForParent,
]);
}
private static function getMappedEvents(array $events): array
{
return array_merge(...array_map(
static fn($event, $function) => self::eventMapper($event, $function),
array_keys($events),
array_values($events)
));
}
private static function eventMapper($event, $function): array
{
return array_reduce(self::getAbles(), static function ($carry, $class) use ($function, $event) {
$carry[str_replace(self::TYPE_PLACEHOLDER, $class, $event)] = $function;
return $carry;
}, []);
}
public static function getMaxAllowedFileSize(): int|string
{
$size = trim(ini_get("upload_max_filesize") ?? '2M');
$last = strtolower($size[strlen($size)-1]);
$size = (float)$size;
switch($last) {
case 'g':
$size *= (1024 * 1024 * 1024); //1073741824
break;
case 'm':
$size *= (1024 * 1024); //1048576
break;
case 'k':
$size *= 1024;
break;
}
return $size;
}
}

31
app/Helpers/CatHelper.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Helpers;
class CatHelper
{
public const CATS = [
'=^._.^=',
'(=`ェ´=)',
'(=^ ◡ ^=)',
'/ᐠ。ꞈ。ᐟ\\',
'/ᐠ.ꞈ.ᐟ\\',
'✧/ᐠ-ꞈ-ᐟ\\',
'(ミචᆽචミ)',
'(=චᆽච=)',
'(=ㅇᆽㅇ=)',
'(=ㅇ༝ㅇ=)',
'₍⸍⸌̣ʷ̣̫⸍̣⸌₎',
'=^ᵒ⋏ᵒ^=',
'( ⓛ ﻌ ⓛ *)',
'(=ↀωↀ=)',
'(=^・ω・^=)',
'(=^・ェ・^=)',
'ㅇㅅㅇ'
];
public static function getCat(): string
{
return self::CATS[array_rand(self::CATS)];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Helpers;
use App;
use Config;
class EnvUpdater
{
public static function bulkSet(array $values): void
{
foreach ($values as $key => $value) {
self::set($key, $value);
}
}
public static function set(string $key, mixed $value): void
{
$currentContents = file_get_contents(App::environmentFilePath());
$keyPosition = strpos($currentContents, "/^{$key}=.*/m");
if ($keyPosition === false) {
$currentContents .= "\n{$key}={$value}";
} else {
$currentContents = preg_replace(
"/^{$key}=.*/m",
$key . '=' . $value,
$currentContents
);
}
file_put_contents(App::environmentFilePath(), $currentContents);
Config::set($key, $value);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Helpers;
use App\Contracts\ScreenshotService;
use App\Models\TimeInterval;
use Faker\Factory;
class FakeScreenshotGenerator
{
private const SCREENSHOT_WIDTH = 1920;
private const SCREENSHOT_HEIGHT = 1080;
public static function runForTimeInterval(TimeInterval|int $timeInterval): void
{
$service = app()->make(ScreenshotService::class);
$tmpFile = tempnam(sys_get_temp_dir(), 'cattr_screenshot');
$image = imagecreatetruecolor(self::SCREENSHOT_WIDTH, self::SCREENSHOT_HEIGHT);
$background = imagecolorallocate($image, random_int(0, 255), random_int(0, 255), random_int(0, 255));
imagefill($image, 0, 0, $background);
\imagejpeg($image, $tmpFile);
imagedestroy($image);
$service->saveScreenshot(
$tmpFile,
$timeInterval
);
unlink($tmpFile);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Helpers;
use Closure;
use Illuminate\Events\Dispatcher as LaravelDispatcher;
class FilterDispatcher extends LaravelDispatcher
{
public static function getRequestFilterName(): string
{
return 'filter.request.' . request()?->route()?->getName();
}
public static function getQueryFilterName(): string
{
return 'filter.query.get.' . request()?->route()?->getName();
}
public static function getQueryAdditionalRelationsFilterName(): string
{
return 'filter.query.with.' . request()?->route()?->getName();
}
public static function getQueryAdditionalRelationsSumFilterName(): string
{
return 'filter.query.withSum.' . request()?->route()?->getName();
}
public static function getSuccessResponseFilterName(): string
{
return 'filter.response.success.' . request()?->route()?->getName();
}
public static function getErrorResponseFilterName(): string
{
return 'filter.response.error.' . request()?->route()?->getName();
}
public static function getValidationFilterName(): string
{
return 'filter.validation.' . request()?->route()?->getName();
}
public static function getAuthFilterName(): string
{
return 'filter.authorize.' . request()?->route()?->getName();
}
public static function getAuthValidationFilterName(): string
{
return 'filter.authorize.validated' . request()?->route()?->getName();
}
public static function getBeforeActionEventName(): string
{
return 'event.before.action.' . request()?->route()?->getName();
}
public static function getAfterActionEventName(): string
{
return 'event.after.action.' . request()?->route()?->getName();
}
public static function getActionFilterName(): string
{
return 'filter.action.' . request()?->route()?->getName();
}
/**
* @inerhitDoc
* @param $event
* @param mixed $payload
* @return mixed
*/
public function process($event, mixed $payload): mixed
{
return $this->dispatch($event, [$payload]);
}
/**
* @inerhitDoc
* @param object|string $event
* @param array $payload
* @param bool $halt
* @return mixed
*/
public function dispatch($event, mixed $payload = [], $halt = false): mixed
{
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
if ($halt && null !== $response) {
return $response;
}
if ($response === false) {
break;
}
$payload[0] = $response;
}
return $halt ? null : ($payload[0] ?? null);
}
/**
* Register an event listener with the dispatcher.
*
* @param Closure|string $listener
* @param bool $wildcard
* @return Closure
*/
public function makeListener($listener, $wildcard = false): callable
{
if (is_string($listener)) {
return $this->createClassListener($listener, $wildcard);
}
return static function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return $listener($event, $payload[0]);
}
return $listener($payload[0]);
};
}
/**
* Create a class based listener using the IoC container.
*
* @param string $listener
* @param bool $wildcard
* @return Closure
*/
public function createClassListener($listener, $wildcard = false): callable
{
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return call_user_func($this->createClassCallable($listener), $event, $payload[0]);
}
return call_user_func_array(
$this->createClassCallable($listener),
$payload
);
};
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Helpers;
use JsonException;
use Module;
class ModuleHelper
{
/**
* Returns information about installed modules.
* @throws JsonException
*/
public static function getModulesInfo(): array
{
foreach (Module::all() as $name => $module) {
$info[] = [
'name' => $name,
'version' => (string)(new Version($name)),
'enabled' => $module->isEnabled()
];
}
return $info ?? [];
}
}

202
app/Helpers/QueryHelper.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
namespace App\Helpers;
use Exception;
use Filter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class QueryHelper
{
private const RESERVED_REQUEST_KEYWORDS = [
'with',
'withCount',
'paginate',
'perPage',
'page',
'with_deleted',
'search',
];
/**
* @throws Exception
*/
public static function apply(Builder $query, Model $model, array $filter = [], bool $first = true): void
{
$table = $model->getTable();
$relations = [];
if (isset($filter['limit'])) {
$query->limit((int)$filter['limit']);
unset($filter['limit']);
}
if (isset($filter['offset'])) {
$query->offset((int)$filter['offset']);
unset($filter['offset']);
}
if (isset($filter['orderBy'])) {
$order_by = $filter['orderBy'];
[$column, $dir] = isset($order_by[1]) ? array_values($order_by) : [$order_by[0], 'asc'];
if (Schema::hasColumn($table, $column)) {
$query->orderBy($column, $dir);
}
unset($filter['orderBy']);
}
if (isset($filter['with'])) {
$query->with(self::getRelationsFilter($filter['with']));
}
if (isset($filter['withCount'])) {
$query->withCount($filter['withCount']);
}
if (isset($filter['withSum'])) {
foreach ($filter['withSum'] as $withSum) {
$query->withSum(...$withSum);
}
}
if (isset($filter['search']['query'], $filter['search']['fields'])) {
$query = self::buildSearchQuery($query, $filter['search']['query'], $filter['search']['fields']);
}
if (isset($filter['where'])) {
foreach ($filter['where'] as $key => $param) {
if (str_contains($key, '.')) {
$params = explode('.', $key);
$domain = array_shift($params);
$filterParam = implode('.', $params);
if (!isset($relations[$domain])) {
$relations[$domain] = [];
}
$relations[$domain][$filterParam] = $param;
} elseif (Schema::hasColumn($table, $key) &&
!in_array($key, static::RESERVED_REQUEST_KEYWORDS, true) &&
!in_array($key, $model->getHidden(), true)
) {
[$operator, $value] = is_array($param) ? array_values($param) : ['=', $param];
if (is_array($value) && $operator !== 'in') {
if ($operator === '=') {
$query->whereIn("$table.$key", $value);
} elseif ($operator === 'between' && count($value) >= 2) {
$query->whereBetween("$table.$key", [$value[0], $value[1]]);
}
} elseif ($operator === 'in') {
$inArgs = is_array($value) ? $value : [$value];
$query->whereIn("$table.$key", $inArgs);
} else {
$query->where("$table.$key", $operator, $value);
}
}
}
}
if (!empty($relations)) {
foreach ($relations as $domain => $filters) {
if (!method_exists($model, $domain)) {
$cls = get_class($model);
throw new RuntimeException("Unknown relation $cls::$domain()");
}
/** @var Relation $relationQuery */
$relationQuery = $model->{$domain}();
$query->whereHas($domain, static function ($q) use ($filters, $relationQuery, $first) {
QueryHelper::apply($q, $relationQuery->getModel(), ['where' => $filters], $first);
});
}
}
}
/**
* @throws Exception
*/
private static function getRelationsFilter(array $filter): array
{
$key = array_search('can', $filter, true);
if ($key !== false) {
array_splice($filter, $key, 1);
Filter::listen(Filter::getActionFilterName(), static function ($data) {
if ($data instanceof Model) {
$data->append('can');
return $data;
}
if ($data instanceof Collection) {
return $data->map(static fn(Model $el) => $el->append('can'));
}
if ($data instanceof AbstractPaginator) {
$data->setCollection($data->getCollection()->map(static fn(Model $el) => $el->append('can')));
return $data;
}
return $data;
});
}
return $filter;
}
/**
* @param Builder $query
* @param string $search
* @param string[] $fields
*
* @return Builder
*/
private static function buildSearchQuery(Builder $query, string $search, array $fields): Builder
{
$value = "%$search%";
return $query->where(static function ($query) use ($value, $fields) {
$field = array_shift($fields);
if (str_contains($field, '.')) {
[$relation, $relationField] = explode('.', $field);
$query->whereHas($relation, static fn(Builder $query) => $query->where($relationField, 'like', $value)
);
} else {
$query->where($field, 'like', $value);
}
foreach ($fields as $field) {
if (str_contains($field, '.')) {
[$relation, $relationField] = explode('.', $field);
$query->orWhereHas($relation, static fn(Builder $query) => $query->where($relationField, 'like', $value)
);
} else {
$query->orWhere($field, 'like', $value);
}
}
});
}
public static function getValidationRules(): array
{
return [
'limit' => 'sometimes|int',
'offset' => 'sometimes|int',
'orderBy' => 'sometimes|array',
'with.*' => 'sometimes|string',
'withCount.*' => 'sometimes|string',
'withSum.*.*' => 'sometimes|string',
'search.query' => 'sometimes|string|nullable',
'search.fields.*' => 'sometimes|string',
'where' => 'sometimes|array',
];
}
}

237
app/Helpers/Recaptcha.php Normal file
View File

@@ -0,0 +1,237 @@
<?php
namespace App\Helpers;
use App\Exceptions\Entities\AuthorizationException;
use Cache;
use GuzzleHttp\Client;
use Request;
use Throwable;
class Recaptcha
{
private string $userEmail;
private bool $solved = false;
/**
* Increment amount of login tries for ip and login
*/
public function incrementCaptchaAmounts(): void
{
if (!$this->captchaEnabled()) {
return;
}
$cacheKey = $this->getCaptchaCacheKey();
if (!Cache::store('octane')->has($cacheKey)) {
Cache::store('octane')->put($cacheKey, 1, config('recaptcha.ttl'));
} else {
Cache::store('octane')->increment($cacheKey);
}
}
/**
* Shows captcha status from config
*
* @return bool
*/
private function captchaEnabled(): bool
{
return config('recaptcha.enabled');
}
/**
* Returns unique for ip and user login key
* @return string
*/
private function getCaptchaCacheKey(): string
{
$ip = Request::ip();
$email = $this->userEmail;
return "AUTH_RECAPTCHA_LIMITER_{$ip}_{$email}_ATTEMPTS";
}
/**
* Forget about tries of user to login
*/
public function clearCaptchaAmounts(): void
{
if (!$this->captchaEnabled()) {
return;
}
Cache::store('octane')->forget($this->getCaptchaCacheKey());
}
/**
* @param array $credentials
* @throws AuthorizationException
*/
public function check(array $credentials): void
{
if ($this->isBanned()) {
$this->incrementBanAmounts();
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_BANNED);
}
$this->userEmail = $credentials['email'];
if (isset($credentials['recaptcha'])) {
$this->solve($credentials['recaptcha']);
}
if ($this->needsCaptcha()) {
$this->incrementBanAmounts();
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_CAPTCHA);
}
}
/**
* Tests if user on ban list
*
* @return bool
*/
private function isBanned(): bool
{
if (!$this->banEnabled()) {
return false;
}
$cacheKey = $this->getBanCacheKey();
$banData = Cache::store('octane')->get($cacheKey, null);
if ($banData === null || !isset($banData['amounts'], $banData['time'])) {
return false;
}
if ($banData['amounts'] < config('recaptcha.ban_attempts')) {
return false;
}
if ($banData['time'] + config('recaptcha.rate_limiter_ttl') < time()) {
Cache::store('octane')->forget($cacheKey);
return false;
}
return true;
}
/**
* Shows ban limiter status from config
*
* @return bool
*/
private function banEnabled(): bool
{
return $this->captchaEnabled() && config('recaptcha.rate_limiter_enabled');
}
/**
* Returns unique for ip key
*
* @return string
*/
private function getBanCacheKey(): string
{
$ip = Request::ip();
return "AUTH_RATE_LIMITER_{$ip}";
}
/**
* Increment amount of tries for ip
*/
private function incrementBanAmounts(): void
{
if (!$this->banEnabled()) {
return;
}
$cacheKey = $this->getBanCacheKey();
$banData = Cache::store('octane')->get($cacheKey);
if ($banData === null || !isset($banData['amounts'])) {
$banData = ['amounts' => 1, 'time' => time()];
} else {
$banData['amounts']++;
}
Cache::store('octane')->put($cacheKey, $banData, config('recaptcha.rate_limiter_ttl'));
}
/**
* Sends request to google with captcha token for verify
*
* @param string $captchaToken
*/
private function solve(string $captchaToken = ''): void
{
if (!$this->captchaEnabled()) {
return;
}
if ($this->solved) {
return;
}
$response = (new Client())->post(config('recaptcha.google_url'), [
'form_params' => [
'secret' => config('recaptcha.secret_key'),
'response' => $captchaToken,
],
]);
if ($response->getStatusCode() !== 200) {
return;
}
$response = $response->getBody();
if ($response === null) {
return;
}
try {
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR);
} catch (Throwable $throwable) {
return;
}
if (isset($data['success']) && $data['success'] === true) {
$this->solved = true;
}
}
/**
* Tests if we need to show captcha to user
*
* @return bool
*/
private function needsCaptcha(): bool
{
if (!$this->captchaEnabled()) {
return false;
}
if ($this->solved) {
return false;
}
$cacheKey = $this->getCaptchaCacheKey();
$attempts = Cache::store('octane')->get($cacheKey, null);
if ($attempts === null) {
return false;
}
if ($attempts <= config('recaptcha.failed_attempts')) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Helpers;
use App\Models\Project;
use App\Models\Task;
use App\Models\TimeInterval;
use App\Models\User;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use JsonException;
use Maatwebsite\Excel\Excel;
use RuntimeException;
class ReportHelper
{
public static string $dateFormat = 'Y-m-d';
public static function getReportFormat(Request $request)
{
return array_flip(self::getAvailableReportFormats())[$request->header('accept')] ?? null;
}
public static function getAvailableReportFormats(): array
{
return [
'csv' => 'text/csv',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pdf' => 'application/pdf',
'xls' => 'application/vnd.ms-excel',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'html' => 'text/html',
];
}
/**
* @param int[] $users
* @param Carbon $startAt
* @param Carbon $endAt
* @param string[] $select
* @return Builder
*/
public static function getBaseQuery(
array $users,
Carbon $startAt,
Carbon $endAt,
array $select = []
): Builder
{
return Project::join('tasks', 'tasks.project_id', '=', 'projects.id')
->join('time_intervals', 'time_intervals.task_id', '=', 'tasks.id')
->join('users', 'time_intervals.user_id', '=', 'users.id')
->select(array_unique(
array_merge($select, [
'time_intervals.id',
'projects.id as project_id',
'projects.name as project_name',
'tasks.id as task_id',
'tasks.task_name as task_name',
'users.id as user_id',
'users.full_name as full_name',
'time_intervals.start_at',
])
))
->where(fn($query) => $query->whereBetween('time_intervals.start_at', [$startAt, $endAt])
->orWhereBetween('time_intervals.end_at', [$startAt, $endAt]))
->whereNull('time_intervals.deleted_at')
->whereIn('users.id', $users)
->groupBy(['tasks.id', 'users.id', 'time_intervals.start_at'])
->orderBy('time_intervals.start_at');
}
/**
* Calculate interval duration in period
* @param CarbonPeriod $period
* @param array $durationByDay
* @return int
*/
public static function getIntervalDurationInPeriod(CarbonPeriod $period, array $durationByDay): int
{
$durationInPeriod = 0;
foreach ($durationByDay as $date => $duration) {
$period->contains($date) && $durationInPeriod += $duration;
}
return $durationInPeriod;
}
/**
* Splits interval by days on which it exists on. Considering timezone of a user if provided.
* @param $interval
* @param $companyTimezone
* @param $userTimezone
* @return array
*/
public static function getIntervalDurationByDay($interval, $companyTimezone, $userTimezone = null): array
{
if ($userTimezone === null) {
$userTimezone = $companyTimezone;
}
$startAt = Carbon::parse($interval->start_at)->shiftTimezone($companyTimezone)->setTimezone($userTimezone);
$endAt = Carbon::parse($interval->end_at)->shiftTimezone($companyTimezone)->setTimezone($userTimezone);
$startDate = $startAt->format(self::$dateFormat);
$endDate = $endAt->format(self::$dateFormat);
$durationByDay = [];
if ($startDate === $endDate) {
$durationByDay[$startDate] = $interval->duration;
} else {
// If interval spans over midnight, divide it at midnight
$startOfDay = $endAt->copy()->startOfDay();
$startDateDuration = $startOfDay->diffInSeconds($startAt);
$durationByDay[$startDate] = $startDateDuration;
$endDateDuration = $endAt->diffInSeconds($startOfDay);
$durationByDay[$endDate] = $endDateDuration;
}
return $durationByDay;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace App\Helpers;
use App\Contracts\ScreenshotService;
use App\Models\TimeInterval;
use Cache;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Builder;
use Storage;
class StorageCleaner
{
public static function getFreeSpace(): float
{
return disk_free_space(Storage::path(ScreenshotService::PARENT_FOLDER));
}
public static function getUsedSpace(): float
{
return config('cleaner.total_space') - self::getFreeSpace();
}
public static function needThinning(): bool
{
return
self::getUsedSpace() * 100 / config('cleaner.total_space')
>= config('cleaner.threshold');
}
/**
* @throws BindingResolutionException
*/
public static function thin($force = false): void
{
if ((!$force && !self::needThinning()) || Cache::store('octane')->get('thinning_now')) {
return;
}
Cache::store('octane')->set('thinning_now', true);
$service = app()->make(ScreenshotService::class);
while (self::getUsedSpace() > self::getWaterlineBorder()) {
$availableScreenshots = self::getAvailableScreenshots();
if (count($availableScreenshots) === 0) {
break;
}
foreach ($availableScreenshots as $screenshot) {
$service->destroyScreenshot($screenshot);
}
}
Cache::store('octane')->set('thinning_now',false);
Cache::store('octane')->set('last_thin',now());
}
/**
* @return array
* @throws BindingResolutionException
*/
public static function getAvailableScreenshots(): array
{
$service = app()->make(ScreenshotService::class);
$collection = self::getScreenshotsCollection();
$result = [];
$i = 0;
foreach ($collection->cursor() as $interval) {
if (Storage::exists($service->getScreenshotPath($interval))) {
$result[] = $interval->id;
}
if ($i >= config('cleaner.page_size')) {
break;
}
}
return $result;
}
/**
* @return int
* @throws BindingResolutionException
*/
public static function countAvailableScreenshots(): int
{
$count = 0;
$service = app()->make(ScreenshotService::class);
$collection = self::getScreenshotsCollection();
foreach ($collection->cursor() as $interval) {
$count += (int)Storage::exists($service->getScreenshotPath($interval));
}
return $count;
}
private static function getWaterlineBorder(): float
{
return config('cleaner.total_space')
* (config('cleaner.threshold') * 0.01)
* (1 - config('cleaner.waterline') * 0.01);
}
private static function getScreenshotsCollection(): Builder|TimeInterval|\Illuminate\Database\Query\Builder
{
return TimeInterval::whereHas('task', function (Builder $query) {
$query->where('important', '=', 0);
})
->whereHas('task.project', function (Builder $query) {
$query->where('important', '=', 0);
})->whereHas('user', function (Builder $query) {
$query->where('permanent_screenshots', '=', 0);
})->orderBy('id');
}
}

114
app/Helpers/Version.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
namespace App\Helpers;
use Composer\InstalledVersions;
use CzProject\GitPhp\Git;
use CzProject\GitPhp\GitException;
use Exception;
use Illuminate\Support\Str;
use Nwidart\Modules\Facades\Module;
use RuntimeException;
use Throwable;
//use Module;
class Version
{
private string $version;
/**
* @throws Throwable
*/
public function __construct(protected ?string $module = null)
{
throw_if($module && !isset(self::getModules()[$module]), new RuntimeException('No such module'));
if ($module) {
$this->version = self::getModules()[$module];
return;
}
$this->version = env('APP_VERSION', 'dev') ?: self::getComposerVersion(base_path());
}
public static function getModules(): array
{
return cache()->rememberForever('app.modules', static function() {
try {
return Module::toCollection()->map(static function (\Nwidart\Modules\Laravel\Module $module) {
$modulePath = $module->getPath();
try {
return self::getComposerVersion($modulePath);
} catch (Throwable) {
try {
return self::getFileVersion($modulePath);
} catch (Throwable) {
try {
return self::getGitVersion($modulePath);
} catch (Throwable) {
return 'dev';
}
}
}
})->toArray();
} catch (Throwable) {
return [];
}
});
}
/**
* @throws Throwable
*/
private static function getComposerVersion(string $path): string
{
throw_unless(file_exists("$path/composer.json"));
$composerConfig = file_get_contents("$path/composer.json");
throw_unless(Str::isJson($composerConfig));
$package = json_decode($composerConfig, true, 512, JSON_THROW_ON_ERROR);
throw_unless(InstalledVersions::isInstalled($package['name']) || isset($package['version']));
return $package['version'] ?? InstalledVersions::getVersion($package['name']);
}
/**
* @throws Throwable
*/
private static function getFileVersion(string $path): string
{
throw_unless(file_exists("$path/module.json"));
$moduleConfig = file_get_contents("$path/module.json");
throw_unless(Str::isJson($moduleConfig));
$module = json_decode($moduleConfig, true, 512, JSON_THROW_ON_ERROR);
throw_unless(isset($module['version']));
return $module['version'];
}
/**
* @throws GitException
*/
private static function getGitVersion(string $path): string
{
$repo = (new Git())->open($path);
$tags = $repo->getTags();
return array_pop($tags);
}
public function __toString(): string
{
return preg_replace('/^v(.*)$/', '$1', $this->version);
}
}