first commit
This commit is contained in:
96
app/Helpers/AttachmentHelper.php
Normal file
96
app/Helpers/AttachmentHelper.php
Normal 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
31
app/Helpers/CatHelper.php
Normal 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)];
|
||||
}
|
||||
}
|
||||
37
app/Helpers/EnvUpdater.php
Normal file
37
app/Helpers/EnvUpdater.php
Normal 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);
|
||||
}
|
||||
}
|
||||
35
app/Helpers/FakeScreenshotGenerator.php
Normal file
35
app/Helpers/FakeScreenshotGenerator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
150
app/Helpers/FilterDispatcher.php
Normal file
150
app/Helpers/FilterDispatcher.php
Normal 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
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
26
app/Helpers/ModuleHelper.php
Normal file
26
app/Helpers/ModuleHelper.php
Normal 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
202
app/Helpers/QueryHelper.php
Normal 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
237
app/Helpers/Recaptcha.php
Normal 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;
|
||||
}
|
||||
}
|
||||
126
app/Helpers/ReportHelper.php
Normal file
126
app/Helpers/ReportHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
124
app/Helpers/StorageCleaner.php
Normal file
124
app/Helpers/StorageCleaner.php
Normal 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
114
app/Helpers/Version.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user