diff --git a/app/Auth/Permission.php b/app/Auth/Permission.php index f1a50783..7fbd3e63 100644 --- a/app/Auth/Permission.php +++ b/app/Auth/Permission.php @@ -3,16 +3,65 @@ namespace App\Auth; use App\Enums\Permission\PermissionEnum; +use App\Models\User; +use Illuminate\Support\Facades\Auth; class Permission { public static function canUploadToSpecialSection(): bool { - return user_can(PermissionEnum::UPLOAD_TO_SPECIAL_SECTION->value); + return self::canUploadToNormalSection() && user_can(PermissionEnum::UPLOAD_TO_SPECIAL_SECTION->value); + } + + public static function canUploadToNormalSection(): bool + { + $user = Auth::user(); + return $user->uploadpos == "yes" && user_can(PermissionEnum::UPLOAD->value); + } + + public static function canViewSpecialSection(): bool + { + return user_can(PermissionEnum::TORRENT_VIEW_SPECIAL->value); } public static function canBeAnonymous(): bool { return user_can(PermissionEnum::BE_ANONYMOUS->value); } + + public static function canSetTorrentHitAndRun(): bool + { + return user_can(PermissionEnum::TORRENT_SET_HR->value); + } + + public static function canSetTorrentPrice(): bool + { + return user_can(PermissionEnum::TORRENT_SET_PRICE->value); + } + + public static function canSetTorrentPosState(): bool + { + return user_can(PermissionEnum::TORRENT_SET_STICKY->value); + } + + public static function canTorrentApprovalAllowAutomatic(): bool + { + return user_can(PermissionEnum::TORRENT_APPROVAL_ALLOW_AUTOMATIC->value); + } + + public static function canManageTorrent(): bool + { + return user_can(PermissionEnum::TORRENT_MANAGE->value); + } + + public static function canPickTorrent(): bool + { + $user = Auth::user(); + return $user->picker == "yes" && self::canManageTorrent() || $user->class >= User::CLASS_SYSOP; + } + + public static function canSetTorrentSpecialTag(): bool + { + return user_can(PermissionEnum::TORRENT_SET_SPECIAL_TAG->value); + } } diff --git a/app/Console/Commands/FireEvent.php b/app/Console/Commands/FireEvent.php index 208d972f..9b6374f5 100644 --- a/app/Console/Commands/FireEvent.php +++ b/app/Console/Commands/FireEvent.php @@ -2,18 +2,7 @@ namespace App\Console\Commands; -use App\Events\NewsCreated; -use App\Events\TorrentCreated; -use App\Events\TorrentDeleted; -use App\Events\TorrentUpdated; -use App\Events\UserCreated; -use App\Events\UserDestroyed; -use App\Events\UserDisabled; -use App\Events\UserEnabled; -use App\Events\UserUpdated; -use App\Models\News; -use App\Models\Torrent; -use App\Models\User; +use App\Enums\ModelEventEnum; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Model; use Nexus\Database\NexusDB; @@ -35,20 +24,6 @@ class FireEvent extends Command */ protected $description = 'Fire an event, options: --name, --idKey --idKeyOld'; - protected array $eventMaps = [ - "torrent_created" => ['event' => TorrentCreated::class, 'model' => Torrent::class], - "torrent_updated" => ['event' => TorrentUpdated::class, 'model' => Torrent::class], - "torrent_deleted" => ['event' => TorrentDeleted::class, 'model' => Torrent::class], - - "user_created" => ['event' => UserCreated::class, 'model' => User::class], - "user_destroyed" => ['event' => UserDestroyed::class, 'model' => User::class], - "user_disabled" => ['event' => UserDisabled::class, 'model' => User::class], - "user_enabled" => ['event' => UserEnabled::class, 'model' => User::class], - "user_updated" => ['event' => UserUpdated::class, 'model' => User::class], - - "news_created" => ['event' => NewsCreated::class, 'model' => News::class], - ]; - /** * Execute the console command. * @@ -60,8 +35,8 @@ class FireEvent extends Command $idKey = $this->option('idKey'); $idKeyOld = $this->option('idKeyOld'); $log = "FireEvent, name: $name, idKey: $idKey, idKeyOld: $idKeyOld"; - if (isset($this->eventMaps[$name])) { - $eventName = $this->eventMaps[$name]['event']; + if (isset(ModelEventEnum::$eventMaps[$name])) { + $eventName = ModelEventEnum::$eventMaps[$name]['event']; $model = unserialize(NexusDB::cache_get($idKey)); if ($model instanceof Model) { $params = [$model]; diff --git a/app/Console/Commands/Test.php b/app/Console/Commands/Test.php index 9cfb8cee..e4601533 100644 --- a/app/Console/Commands/Test.php +++ b/app/Console/Commands/Test.php @@ -3,6 +3,9 @@ namespace App\Console\Commands; use App\Models\PersonalAccessToken; +use App\Models\Torrent; +use App\Models\User; +use App\Repositories\UploadRepository; use Illuminate\Console\Command; use NexusPlugin\Menu\Filament\MenuItemResource\Pages\ManageMenuItems; use NexusPlugin\Menu\MenuRepository; @@ -15,6 +18,7 @@ use NexusPlugin\StickyPromotion\Models\StickyPromotionParticipator; use NexusPlugin\Tracker\TrackerRepository; use NexusPlugin\Work\Models\RoleWork; use NexusPlugin\Work\WorkRepository; +use Stichoza\GoogleTranslate\GoogleTranslate; class Test extends Command { @@ -49,8 +53,12 @@ class Test extends Command */ public function handle() { - $result = getLogFile(); - dd($result); + $a = ['acb' => 2]; + + if ($a = isset($a['ab'])) { + $this->info("isset ab = true"); + } + dd($a); } } diff --git a/app/Console/Commands/TranslateLang.php b/app/Console/Commands/TranslateLang.php new file mode 100644 index 00000000..94f8e645 --- /dev/null +++ b/app/Console/Commands/TranslateLang.php @@ -0,0 +1,214 @@ +argument('source'); + $target = $this->argument('target'); + $filename = $this->argument('filename'); + $this->ignoreKeys = array_filter(explode(',', $this->option('ignore'))); + $this->cachePath = storage_path("framework/lang-translate-cache.{$source}.{$target}.json"); + + $this->loadCache(); + + $this->tr = new GoogleTranslate(); + $this->tr->setSource($source); + $this->tr->setTarget($target); + + $langPath = resource_path('lang'); + + //谷歌使用的是 -, 本地使用 _ + $source = str_replace("-", "_", $source); + $target = str_replace("-", "_", $target); + + $dir = "{$langPath}/{$source}"; + if ($filename) { + // 👇 指定具体文件翻译 + $this->translateSpecificFile($filename, $source, $target); + } else { + // 👇 未指定时,用户确认是否翻译所有文件 + $answer = $this->ask("你没有指定文件名,是否翻译目录 $dir 下所有语言文件?请输入 yes 确认"); + if (strtolower($answer) === 'yes') { + foreach (File::files("{$langPath}/{$source}") as $file) { + if ($file->getExtension() === 'php') { + $this->translatePhpFile($file->getPathname(), $source, $target); + } + } + if ($this->option('json')) { + $jsonFile = "{$langPath}/{$source}.json"; + if (file_exists($jsonFile)) { + $this->translateJsonFile($jsonFile, $source, $target); + } + } + } else { + // 👇 用户的输入被当作 filename 处理 + $this->translateSpecificFile($answer, $source, $target); + } + } + + $this->saveCache(); + + $this->info("🎉 $source => $target Done !"); + return 0; + } + + protected function translatePhpFile($sourceFile, $sourceLang, $targetLang) + { + $relativePath = basename($sourceFile); + $targetFile = resource_path("lang/{$targetLang}/{$relativePath}"); + + $data = require $sourceFile; + $translated = $this->translateArray($data); + + $export = var_export($translated, true); + if ($this->option('dry-run')) { + $this->line("🔍 Would write to: $targetFile\n$export\n"); + } else { + if (!file_exists(dirname($targetFile))) { + mkdir(dirname($targetFile), 0755, true); + } + file_put_contents($targetFile, "info("✅ Wrote translated file: $targetFile"); + } + } + + protected function translateJsonFile($jsonFile, $sourceLang, $targetLang) + { + $targetFile = resource_path("lang/{$targetLang}.json"); + $content = json_decode(file_get_contents($jsonFile), true); + $translated = []; + + foreach ($content as $key => $value) { + if (in_array($key, $this->ignoreKeys)) { + $translated[$key] = $value; + continue; + } + + $translated[$key] = $this->translateText($value); + } + + $pretty = $this->json_encode_pretty($translated); + + if ($this->option('dry-run')) { + $this->line("🔍 Would write to: $targetFile\n$pretty\n"); + } else { + file_put_contents($targetFile, $pretty); + $this->info("✅ Wrote translated JSON: $targetFile"); + } + } + + protected function translateArray(array $data) + { + $result = []; + foreach ($data as $key => $value) { + if (in_array($key, $this->ignoreKeys)) { + $result[$key] = $value; + continue; + } + + if (is_array($value)) { + $result[$key] = $this->translateArray($value); + } else { + $result[$key] = $this->translateText($value); + } + } + return $result; + } + + protected function translateText(string $text): string + { + if (isset($this->cache[$text])) { + $this->line("⚡️ Cached: $text => {$this->cache[$text]}"); + return $this->cache[$text]; + } + + try { + $translated = $this->tr->translate($text); + $this->cache[$text] = $translated; + $this->line("🌍 $text => $translated"); + return $translated; + } catch (\Exception $e) { + $this->warn("❌ Failed to translate: $text"); + return $text; + } + } + + protected function translateSpecificFile($filename, $source, $target) + { + $langPath = resource_path("lang"); + + if (str_ends_with($filename, '.json')) { + $jsonPath = "{$langPath}/{$filename}"; + if (!file_exists($jsonPath)) { + $jsonPath = "{$langPath}/{$source}.json"; + } + if (file_exists($jsonPath)) { + $this->translateJsonFile($jsonPath, $source, $target); + } else { + $this->error("❌ JSON 文件未找到:$filename"); + } + } else { + $phpPath = "{$langPath}/{$source}/$filename"; + if (!str_ends_with($filename, '.php')) { + $phpPath .= '.php'; + } + if (file_exists($phpPath)) { + $this->translatePhpFile($phpPath, $source, $target); + } else { + $this->error("❌ PHP 语言文件未找到:$filename"); + } + } + } + + protected function loadCache() + { + if (file_exists($this->cachePath)) { + $this->cache = json_decode(file_get_contents($this->cachePath), true); + } + } + + protected function saveCache() + { + if (!$this->option('dry-run')) { + file_put_contents($this->cachePath, json_encode($this->cache, JSON_UNESCAPED_UNICODE)); + } + } + + protected function json_encode_pretty($data, int $indentSize = 4): string + { + // 默认格式化(PHP 默认是 2 空格) + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + // 自定义缩进 + $indentChar = str_repeat(' ', $indentSize); + + // 将默认的 2 空格缩进替换为自定义缩进 + $formatted = preg_replace_callback('/^( +)/m', function ($matches) use ($indentChar) { + $level = strlen($matches[1]) / 2; + return str_repeat($indentChar, (int)$level); + }, $json); + + return $formatted; + } + +} diff --git a/app/Enums/ModelEventEnum.php b/app/Enums/ModelEventEnum.php new file mode 100644 index 00000000..32de3dd6 --- /dev/null +++ b/app/Enums/ModelEventEnum.php @@ -0,0 +1,44 @@ + ['event' => TorrentCreated::class, 'model' => Torrent::class], + self::TORRENT_UPDATED => ['event' => TorrentUpdated::class, 'model' => Torrent::class], + self::TORRENT_DELETED => ['event' => TorrentDeleted::class, 'model' => Torrent::class], + + self::USER_CREATED => ['event' => UserCreated::class, 'model' => User::class], + self::USER_UPDATED => ['event' => UserUpdated::class, 'model' => User::class], + self::USER_DELETED => ['event' => UserDeleted::class, 'model' => User::class], + self::USER_ENABLED => ['event' => UserEnabled::class, 'model' => User::class], + self::USER_DISABLED => ['event' => UserDisabled::class, 'model' => User::class], + + self::NEWS_CREATED => ['event' => NewsCreated::class, 'model' => News::class], + ]; +} diff --git a/app/Enums/Permission/PermissionEnum.php b/app/Enums/Permission/PermissionEnum.php index d33ed4c4..f6689d96 100644 --- a/app/Enums/Permission/PermissionEnum.php +++ b/app/Enums/Permission/PermissionEnum.php @@ -7,5 +7,15 @@ enum PermissionEnum: string { case BE_ANONYMOUS = 'beanonymous'; case TORRENT_LIST = 'torrent:list'; + case TORRENT_VIEW = 'torrent:view'; + case TORRENT_VIEW_SPECIAL = 'view_special_torrent'; + case TORRENT_SET_HR = 'torrent_hr'; + case TORRENT_SET_PRICE = 'torrent-set-price'; + case TORRENT_SET_STICKY = 'torrentsticky'; + case TORRENT_MANAGE = 'torrentmanage'; + case TORRENT_APPROVAL_ALLOW_AUTOMATIC = 'torrent-approval-allow-automatic'; + case TORRENT_SET_SPECIAL_TAG = 'torrent-set-special-tag'; case UPLOAD = 'upload'; + + case USER_VIEW = "user:view"; } diff --git a/app/Events/UserDestroyed.php b/app/Events/UserDeleted.php similarity index 97% rename from app/Events/UserDestroyed.php rename to app/Events/UserDeleted.php index c4cf2d04..099624d3 100644 --- a/app/Events/UserDestroyed.php +++ b/app/Events/UserDeleted.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class UserDestroyed +class UserDeleted { use Dispatchable, InteractsWithSockets, SerializesModels; diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index fd74df35..26e30adc 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -101,6 +101,9 @@ class Handler extends ExceptionHandler if (config('app.debug')) { $data['trace'] = $trace; } + if ($e instanceof \Error || $e instanceof \ErrorException) { + do_log(sprintf(get_class($e) . ": %s, trace: %s", $msg, $e->getTraceAsString()), "error"); + } return new JsonResponse( fail($msg, $data), $httpStatusCode, diff --git a/app/Filament/Resources/Section/SectionResource.php b/app/Filament/Resources/Section/SectionResource.php index 9d40d58d..a8001f47 100644 --- a/app/Filament/Resources/Section/SectionResource.php +++ b/app/Filament/Resources/Section/SectionResource.php @@ -15,6 +15,7 @@ use Filament\Tables\Table; use Filament\Tables; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; +use Illuminate\Validation\Rule; class SectionResource extends Resource { @@ -59,8 +60,13 @@ class SectionResource extends Resource ->schema([ Forms\Components\TextInput::make('name') ->label(__('label.search_box.name')) - ->rules('alpha_dash') - ->required() + ->rules(function ($record) { + return [ + 'required', + 'alpha_dash:ascii', + Rule::unique('searchbox', 'name')->ignore($record?->id) + ]; + }) , Forms\Components\TextInput::make('catsperrow') ->label(__('label.search_box.catsperrow')) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 2ff696a8..cc467a9b 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,25 +4,34 @@ namespace App\Http\Controllers; use App\Exceptions\InsufficientPermissionException; use App\Models\Setting; +use App\Utils\ApiQueryBuilder; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\MissingValue; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; -/** - * @OA\Info( - * title="NexusPHP API", - * version="1.0" - * ) - */ class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; - public function success($data, $msg = null) + protected ?array $extraFields = null; + protected ?array $extraSettingNames = null; + + /** + * 返回成功信息,这里是旧方法,大部分情况下 $data 已经是 JsonResource + * 但很多地方有使用,历史原因保留不动,尽量使用 successJsonResource + * + * @param $data + * @param $msg + * @return array + */ + public function success($data, $msg = null): array { if (is_null($msg)) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); @@ -32,6 +41,34 @@ class Controller extends BaseController return success($msg, $data); } + /** + * 返回成功信息,对于不是 JsonResource 的数据,进行包装。返回的数据在 data.data 中 + * + * @param $data + * @param $msg + * @return array + */ + public function successJsonResource($data, $msg = null): array + { + if (is_null($msg)) { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + $caller = $backtrace[1]; + $msg = $this->getReturnMsg($caller); + } + if ($data instanceof JsonResource) { + return $this->success($data, $msg); + } + $resource = new JsonResource($data); + return $this->success($resource, $msg); + } + + /** + * 返回失败信息,目前对于失败信息不需要包装 + * + * @param $data + * @param $msg + * @return array + */ public function fail($data, $msg = null) { if (is_null($msg)) { @@ -78,6 +115,31 @@ class Controller extends BaseController return [$perPage, ['*'], 'page', $page]; } + protected function hasExtraField($field): bool + { + if ($this->extraFields === null) { + $extraFieldsStr = request()->input("extra_fields", ''); + $this->extraFields = explode(',', $extraFieldsStr); + } + do_log(sprintf("field: %s, extraFields: %s", $field, json_encode($this->extraFields))); + return in_array($field, $this->extraFields); + } + protected function appendExtraSettings(array &$additional, array $names): void + { + if ($this->extraSettingNames === null) { + $extraSettingStr = request()->input("extra_settings", ''); + $this->extraSettingNames = explode(',', $extraSettingStr); + } + $results = []; + foreach ($names as $name) { + if (in_array($name, $this->extraSettingNames)) { + $results[$name] = get_setting($name); + } + } + if (!empty($results)) { + $additional['extra_settings'] = $results; + } + } } diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index b05810aa..17c2c509 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -27,14 +27,12 @@ class TokenController extends Controller $user = Auth::user(); $count = $user->tokens()->count(); if ($count >= 5) { - throw new NexusException("Token limit exceeded"); + throw new NexusException(nexus_trans("token.maximum_allow_number_reached")); } $newAccessToken = $user->createToken($request->name, $request->permissions); - PersonalAccessTokenPlain::query()->create([ - 'access_token_id' => $newAccessToken->accessToken->getKey(), - 'plain_text_token' => $newAccessToken->plainTextToken, - ]); - return $this->success(true); + $tokenText = $newAccessToken->plainTextToken; + $msg = nexus_trans("token.create_success_tip", ['token' => $tokenText]); + return $this->successJsonResource(['token' => $tokenText], $msg); } catch (\Exception $exception) { return $this->fail(false, $exception->getMessage()); } @@ -47,11 +45,7 @@ class TokenController extends Controller 'id' => 'required|integer', ]); $user = Auth::user(); - $token = $user->tokens()->where("id", $request->id)->first(); - if ($token) { - PersonalAccessTokenPlain::query()->where("access_token_id", $token->id)->delete(); - $token->delete(); - } + $user->tokens()->where("id", $request->id)->delete(); return $this->success(true); } catch (\Exception $exception) { return $this->fail(false, $exception->getMessage()); diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php index 8ae4c899..ffba7767 100644 --- a/app/Http/Controllers/ToolController.php +++ b/app/Http/Controllers/ToolController.php @@ -6,6 +6,7 @@ use App\Models\PluginStore; use App\Repositories\ToolRepository; use App\Repositories\UploadRepository; use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; use Symfony\Component\Process\Process; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -31,9 +32,9 @@ class ToolController extends Controller public function test(Request $request) { - $rep = new UploadRepository(); - $result = $rep->listSections(); - return $result; + $result = ['id' => 1]; + $resource = new JsonResource($result); + return $this->success($resource); } } diff --git a/app/Http/Controllers/TorrentController.php b/app/Http/Controllers/TorrentController.php index 9fb0615b..0883ed26 100644 --- a/app/Http/Controllers/TorrentController.php +++ b/app/Http/Controllers/TorrentController.php @@ -11,7 +11,10 @@ use App\Models\TorrentDenyReason; use App\Models\TorrentOperationLog; use App\Models\User; use App\Repositories\TorrentRepository; +use App\Repositories\UploadRepository; use Illuminate\Http\Request; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; class TorrentController extends Controller @@ -23,17 +26,10 @@ class TorrentController extends Controller $this->repository = $repository; } - public function index(Request $request) + public function index(Request $request, string $section = null) { - $params = $request->all(); - $params['visible'] = Torrent::VISIBLE_YES; - $params['category_mode'] = Setting::get('main.browsecat'); - $result = $this->repository->getList($params, Auth::user()); + $result = $this->repository->getList($request, Auth::user(), $section); $resource = TorrentResource::collection($result); -// $resource->additional([ -// 'page_title' => nexus_trans('torrent.index.page_title'), -// ]); - return $this->success($resource); } @@ -41,18 +37,21 @@ class TorrentController extends Controller * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return array */ public function store(Request $request) { - // + $uploadRep = new UploadRepository(); + $newTorrent = $uploadRep->upload($request); + $resource = new JsonResource(["id" => $newTorrent->id]); + return $this->success($resource); } /** * Display the specified resource. * * @param int $id - * @return \Illuminate\Http\Response + * @return array */ public function show($id) { @@ -60,17 +59,15 @@ class TorrentController extends Controller * @var User */ $user = Auth::user(); - $result = $this->repository->getDetail($id, $user); - $isBookmarked = $user->bookmarks()->where('torrentid', $id)->exists(); - - $resource = new TorrentResource($result); - $resource->additional([ -// 'page_title' => nexus_trans('torrent.show.page_title'), -// 'field_labels' => Torrent::getFieldLabels(), - 'is_bookmarked' => (int)$isBookmarked, - 'bonus_reward_values' => Torrent::BONUS_REWARD_VALUES, - ]); - + $torrent = $this->repository->getDetail($id, $user); + $resource = new TorrentResource($torrent); + $additional = []; + if ($this->hasExtraField('bonus_reward_values')) { + $additional['bonus_reward_values'] = Torrent::BONUS_REWARD_VALUES; + } + $extraSettingsNames = ['torrent.claim_torrent_user_counts_up_limit']; + $this->appendExtraSettings($additional, $extraSettingsNames); + $resource->additional($additional); return $this->success($resource); } diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index a2be43e8..7ffcd2ea 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Resources\SearchBoxResource; +use App\Http\Resources\TorrentResource; use App\Repositories\SearchBoxRepository; use App\Repositories\UploadRepository; use Illuminate\Http\Request; @@ -26,10 +27,4 @@ class UploadController extends Controller return $this->success($resource); } - public function upload(Request $request) - { - $user = $request->user(); - return $this->success("OK"); - } - } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 967de3d8..f3d0b949 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -15,6 +15,7 @@ use App\Repositories\UserRepository; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; +use League\OAuth2\Server\Grant\AuthCodeGrant; class UserController extends Controller { @@ -64,10 +65,15 @@ class UserController extends Controller * @param int $id * @return array */ - public function show($id) + public function show($id = null) { - $result = $this->repository->getDetail($id); - return $this->success($result); + $currentUser = Auth::user(); + if ($id === null) { + $id = $currentUser->id; + } + $result = $this->repository->getDetail($id, $currentUser); + $resource = new UserResource($result); + return $this->success($resource); } /** diff --git a/app/Http/Resources/BaseResource.php b/app/Http/Resources/BaseResource.php new file mode 100644 index 00000000..fc4347be --- /dev/null +++ b/app/Http/Resources/BaseResource.php @@ -0,0 +1,25 @@ +whenInclude($field, "include_fields"); + } + + private function whenInclude($field, $prefix): bool + { + $fields = request()->input("$prefix." . $this->getResourceName()); + if (!$fields) { + return false; + } + $fieldsArr = explode(',', $fields); + return in_array($field, $fieldsArr); + } +} diff --git a/app/Http/Resources/MedalResource.php b/app/Http/Resources/MedalResource.php index 043d9aee..a1b7fc71 100644 --- a/app/Http/Resources/MedalResource.php +++ b/app/Http/Resources/MedalResource.php @@ -22,10 +22,15 @@ class MedalResource extends JsonResource 'image_large' => $this->image_large, 'image_small' => $this->image_small, 'price' => $this->price, + 'price_human' => number_format($this->price), 'duration' => $this->duration, 'description' => $this->description, 'expire_at' => $this->whenPivotLoaded('user_medals', function () {return $this->pivot->expire_at;}), 'user_medal_id' => $this->whenPivotLoaded('user_medals', function () {return $this->pivot->id;}), + 'wearing_status' => $this->whenPivotLoaded('user_medals', function () {return $this->pivot->status;}), + 'wearing_status_text' => $this->whenPivotLoaded('user_medals', function () { + return nexus_trans("medal.wearing_status_text." . $this->pivot->status); + }), ]; } } diff --git a/app/Http/Resources/SearchBoxResource.php b/app/Http/Resources/SearchBoxResource.php index 6dcdd4ce..906781f3 100644 --- a/app/Http/Resources/SearchBoxResource.php +++ b/app/Http/Resources/SearchBoxResource.php @@ -19,23 +19,20 @@ class SearchBoxResource extends JsonResource $searchBox = $this->resource; $out = [ 'id' => $this->id, - 'name' => $this->displaySectionName, + 'name' => $this->name, + 'display_name' => $this->displaySectionName, 'categories' => CategoryResource::collection($this->whenLoaded('categories')), 'tags' => TagResource::collection($this->whenLoaded('tags')), ]; if ($searchBox->showsubcat) { $subCategories = []; - $lang = get_langfolder_cookie(); $fields = array_keys(SearchBox::$taxonomies); - if (!empty($searchBox->extra['taxonomy_labels'])) { - $fields = array_column($searchBox->extra['taxonomy_labels'], 'torrent_field'); - } foreach ($fields as $field) { $relationName = "taxonomy_$field"; if ($searchBox->relationLoaded($relationName)) { $subCategories[] = [ 'field' => $field, - 'label' => $item['display_text'][$lang] ?? (nexus_trans("searchbox.sub_category_{$field}_label") ?: ucfirst($field)), + 'label' => $searchBox->getTaxonomyLabel($field), 'data' => MediaResource::collection($searchBox->{$relationName}), ]; } diff --git a/app/Http/Resources/TorrentExtraResource.php b/app/Http/Resources/TorrentExtraResource.php new file mode 100644 index 00000000..442399d5 --- /dev/null +++ b/app/Http/Resources/TorrentExtraResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'descr' => $this->descr, + 'media_info' => $this->media_info, + 'media_info_summary' => $this->media_info_summary, + 'nfo' => $this->nfo, + ]; + } +} diff --git a/app/Http/Resources/TorrentResource.php b/app/Http/Resources/TorrentResource.php index 55ab38c9..5c53b073 100644 --- a/app/Http/Resources/TorrentResource.php +++ b/app/Http/Resources/TorrentResource.php @@ -3,14 +3,21 @@ namespace App\Http\Resources; use App\Models\Attachment; +use App\Models\SearchBox; use App\Models\Torrent; +use App\Repositories\TorrentRepository; use Carbon\CarbonInterface; +use Elasticsearch\Endpoints\Search; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Facades\Auth; use Nexus\Nexus; +use Illuminate\Http\Request; -class TorrentResource extends JsonResource +class TorrentResource extends BaseResource { - protected $imageTypes = ['image', 'attachment']; + const NAME = 'torrent'; + + protected static TorrentRepository $torrentRep; /** * Transform the resource into an array. @@ -24,72 +31,82 @@ class TorrentResource extends JsonResource 'id' => $this->id, 'name' => $this->name, 'filename' => $this->filename, + 'hash' => preg_replace_callback('/./s', [$this, "hex_esc"], $this->info_hash), + 'cover' => $this->cover, 'small_descr' => $this->small_descr, - 'comments' => $this->comments, + 'category' => $this->category, + 'category_info' => new CategoryResource($this->whenLoaded('basic_category')), + 'size' => $this->size, 'size_human' => mksize($this->size), - 'added' => $this->added->toDateTimeString(), - 'added_human' => $this->added->format('Y-m-d H:i'), - 'ttl' => $this->added->diffForHumans(['syntax' => CarbonInterface::DIFF_ABSOLUTE]), - 'leechers' => $this->leechers, - 'seeders' => $this->seeders, - 'times_completed' => $this->times_completed, - 'numfiles' => $this->numfiles, + 'added' => format_datetime($this->added), + 'added_human' => gettime($this->added), + 'numfiles' => $this->numfiles ?: 0, + 'leechers' => $this->leechers ?: 0, + 'seeders' => $this->seeders ?: 0, + 'times_completed' => $this->times_completed ?: 0, + 'views' => $this->views ?: 0, + 'hits' => $this->hits ?: 0, + 'comments' => $this->comments ?: 0, + 'pos_state' => $this->pos_state, + 'pos_state_until' => format_datetime($this->pos_state_until), + 'pos_state_until_human' => gettime($this->pos_state_until), 'sp_state' => $this->sp_state, 'sp_state_real' => $this->sp_state_real, 'promotion_info' => $this->promotionInfo, - 'hr' => $this->hr, + 'hr' => $this->hr ?: 0, 'pick_type' => $this->picktype, 'pick_time' => $this->picktime, 'pick_info' => $this->pickInfo, - 'download_url' => $this->download_url, - 'user' => new UserResource($this->whenLoaded('user')), 'anonymous' => $this->anonymous, - 'basic_category' => new CategoryResource($this->whenLoaded('basic_category')), + 'last_action' => format_datetime($this->last_action), + 'last_action_human' => gettime($this->last_action), + 'thank_users_count' => $this->whenCounted('thank_users'), + 'reward_logs_count' => $this->whenCounted('reward_logs'), + 'claims_count' => $this->whenCounted('claims'), + 'has_bookmarked' => $this->whenHas('has_bookmarked'), + 'has_claimed' => $this->whenHas('has_claimed'), + 'has_thanked' => $this->whenHas('has_thanked'), + 'has_rewarded' => $this->whenHas('has_rewarded'), + 'description' => $this->whenHas('description'), + 'images' => $this->whenHas('images'), + 'download_url' => $this->whenHas('download_url'), + 'user' => new UserResource($this->whenLoaded('user')), + 'extra' => new TorrentExtraResource($this->whenLoaded('extra')), 'tags' => TagResource::collection($this->whenLoaded('tags')), 'thanks' => ThankResource::collection($this->whenLoaded('thanks')), 'reward_logs' => RewardResource::collection($this->whenLoaded('reward_logs')), ]; - if ($this->cover) { - $cover = $this->cover; - } else { - $descriptionArr = format_description($this->descr); - $cover = get_image_from_description($descriptionArr, true); - } - $out['cover'] = resize_image($cover, 100, 100); - if ($request->routeIs('torrents.show')) { - if (!isset($descriptionArr)) { - $descriptionArr = format_description($this->descr); + $subCategories = []; + foreach (SearchBox::$taxonomies as $field => $info) { + $relation = "basic_$field"; + if ($this->resource->{$field} > 0 && $this->resource->relationLoaded($relation)) { + $subCategories[$field] = [ + 'label' => $this->resource->getSubCategoryLabel($field), + 'value' => $this->resource->{$relation}->name ?? '', + ]; } - $baseInfo = [ - ['label' => nexus_trans('torrent.show.size'), 'value' => mksize($this->size)], - ]; - foreach (Torrent::getBasicInfo() as $relation => $text) { - if ($info = $this->whenLoaded($relation)) { - $baseInfo[] = ['label' => $text, 'value' => $info->name]; - } - } - $out['base_info'] = $baseInfo; - - $out['description'] = $descriptionArr; - - $out['images'] = get_image_from_description($descriptionArr); - - $out['thank_users_count'] = $this->thank_users_count; - $out['peers_count'] = $this->peers_count; - $out['reward_logs_count'] = $this->reward_logs_count; } - if (nexus()->isPlatformAdmin()) { - $out['details_url'] = sprintf('%s/details.php?id=%s', getSchemeAndHttpHost(), $this->id); - } - -// $out['upload_peers_count'] = $this->upload_peers_count; -// $out['download_peers_count'] = $this->download_peers_count; -// $out['finish_peers_count'] = $this->finish_peers_count; - + $out['sub_categories'] = empty($subCategories) ? null : $subCategories; return $out; } + protected function getResourceName(): string + { + return self::NAME; + } + + private function getTorrentRep(): TorrentRepository + { + if (!isset(self::$torrentRep)) { + self::$torrentRep = new TorrentRepository(); + } + return self::$torrentRep; + } + + protected function hex_esc($matches) { + return sprintf("%02x", ord($matches[0])); + } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index f5b1ad87..d4f3b8a6 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -6,6 +6,7 @@ use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { + const NAME = 'user'; /** * Transform the resource into an array. * @@ -21,7 +22,9 @@ class UserResource extends JsonResource 'status' => $this->status, 'enabled' => $this->enabled, 'added' => format_datetime($this->added), + 'added_human' => gettime($this->added), 'last_access' => format_datetime($this->last_access), + 'last_access_human' => gettime($this->last_access), 'class' => $this->class, 'class_text' => $this->class_text, 'avatar' => $this->avatar, @@ -31,8 +34,10 @@ class UserResource extends JsonResource 'uploaded_text' => mksize($this->uploaded), 'downloaded' => $this->downloaded, 'downloaded_text' => mksize($this->downloaded), - 'bonus' => number_format($this->seedbonus, 1), - 'seed_points' => number_format($this->seed_points, 1), + 'bonus' => floatval($this->seedbonus), + 'bonus_human' => number_format($this->seedbonus, 1), + 'seed_points' => floatval($this->seed_points), + 'seed_points_human' => number_format($this->seed_points, 1), 'seedtime' => $this->seedtime, 'seedtime_text' => mkprettytime($this->seedtime), 'leechtime' => $this->leechtime, @@ -40,27 +45,27 @@ class UserResource extends JsonResource 'inviter' => new UserResource($this->whenLoaded('inviter')), 'valid_medals' => MedalResource::collection($this->whenLoaded('valid_medals')), ]; - if ($request->routeIs('user.me')) { - $out['downloaded_human'] = mksize($this->downloaded); - $out['uploaded_human'] = mksize($this->uploaded); - $out['seed_time'] = mkprettytime($this->seedtime); - $out['leech_time'] = mkprettytime($this->leechtime); - $out['share_ratio'] = get_share_ratio($this->uploaded, $this->downloaded); - $out['comments_count'] = $this->comments_count; - $out['posts_count'] = $this->posts_count; - $out['torrents_count'] = $this->torrents_count; - $out['seeding_torrents_count'] = $this->seeding_torrents_count; - $out['leeching_torrents_count'] = $this->leeching_torrents_count; - $out['completed_torrents_count'] = $this->completed_torrents_count; - $out['incomplete_torrents_count'] = $this->incomplete_torrents_count; - } - if ($request->routeIs("oauth.user_info")) { - $out['name'] = $this->username; - } - - if (nexus()->isPlatformAdmin() && $request->routeIs('users.show')) { - $out['two_step_secret'] = $this->two_step_secret; - } +// if ($request->routeIs('user.me')) { +// $out['downloaded_human'] = mksize($this->downloaded); +// $out['uploaded_human'] = mksize($this->uploaded); +// $out['seed_time'] = mkprettytime($this->seedtime); +// $out['leech_time'] = mkprettytime($this->leechtime); +// $out['share_ratio'] = get_share_ratio($this->uploaded, $this->downloaded); +// $out['comments_count'] = $this->comments_count; +// $out['posts_count'] = $this->posts_count; +// $out['torrents_count'] = $this->torrents_count; +// $out['seeding_torrents_count'] = $this->seeding_torrents_count; +// $out['leeching_torrents_count'] = $this->leeching_torrents_count; +// $out['completed_torrents_count'] = $this->completed_torrents_count; +// $out['incomplete_torrents_count'] = $this->incomplete_torrents_count; +// } +// if ($request->routeIs("oauth.user_info")) { +// $out['name'] = $this->username; +// } +// +// if (nexus()->isPlatformAdmin() && $request->routeIs('users.show')) { +// $out['two_step_secret'] = $this->two_step_secret; +// } return $out; } diff --git a/app/Listeners/DeductUserBonusWhenTorrentDeleted.php b/app/Listeners/DeductUserBonusWhenTorrentDeleted.php index 403987ab..0a0c2304 100644 --- a/app/Listeners/DeductUserBonusWhenTorrentDeleted.php +++ b/app/Listeners/DeductUserBonusWhenTorrentDeleted.php @@ -5,7 +5,7 @@ namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; -class DeductUserBonusWhenTorrentDeleted +class DeductUserBonusWhenTorrentDeleted implements ShouldQueue { /** * Create the event listener. diff --git a/app/Listeners/SendEmailNotificationWhenTorrentCreated.php b/app/Listeners/SendEmailNotificationWhenTorrentCreated.php new file mode 100644 index 00000000..64f05a06 --- /dev/null +++ b/app/Listeners/SendEmailNotificationWhenTorrentCreated.php @@ -0,0 +1,30 @@ +model; + $uploadRepo = new UploadRepository(); + $result = $uploadRepo->sendEmailNotification($torrent); + do_log("torrent: $torrent->id, sendEmailNotification result: " . var_export($result, true)); + } +} diff --git a/app/Listeners/SyncTorrentToEs.php b/app/Listeners/SyncTorrentToElasticsearch.php similarity index 89% rename from app/Listeners/SyncTorrentToEs.php rename to app/Listeners/SyncTorrentToElasticsearch.php index ad4f991d..40915e8a 100644 --- a/app/Listeners/SyncTorrentToEs.php +++ b/app/Listeners/SyncTorrentToElasticsearch.php @@ -8,7 +8,7 @@ use App\Repositories\ToolRepository; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; -class SyncTorrentToEs implements ShouldQueue +class SyncTorrentToElasticsearch implements ShouldQueue { public $tries = 3; @@ -32,6 +32,10 @@ class SyncTorrentToEs implements ShouldQueue public function handle($event) { $id = $event->model?->id ?? 0; + if ($id == 0) { + do_log("event: " . get_class($event) . " no model id", 'error'); + return; + } $searchRep = new SearchRepository(); $result = $searchRep->updateTorrent($id); do_log(sprintf("updateTorrent: %s result: %s", $id, var_export($result, true))); diff --git a/app/Listeners/SyncTorrentToMeilisearch.php b/app/Listeners/SyncTorrentToMeilisearch.php new file mode 100644 index 00000000..1ba681ea --- /dev/null +++ b/app/Listeners/SyncTorrentToMeilisearch.php @@ -0,0 +1,34 @@ +model->id ?? 0; + if ($id == 0) { + do_log("event: " . get_class($event) . " no model id", 'error'); + return; + } + $meiliSearch = new MeiliSearchRepository(); + $result = $meiliSearch->doImportFromDatabase($id); + do_log(sprintf("doImportFromDatabase: %s result: %s", $id, var_export($result, true))); + } +} diff --git a/app/Models/BonusLogs.php b/app/Models/BonusLogs.php index 7550a386..149e05cf 100644 --- a/app/Models/BonusLogs.php +++ b/app/Models/BonusLogs.php @@ -45,6 +45,7 @@ class BonusLogs extends NexusModel const BUSINESS_TYPE_TORRENT_BE_DOWNLOADED = 1001; const BUSINESS_TYPE_RECEIVE_REWARD = 1002; const BUSINESS_TYPE_RECEIVE_GIFT = 1003; + const BUSINESS_TYPE_UPLOAD_TORRENT = 1004; public static array $businessTypes = [ self::BUSINESS_TYPE_CANCEL_HIT_AND_RUN => ['text' => 'Cancel H&R'], @@ -73,6 +74,7 @@ class BonusLogs extends NexusModel self::BUSINESS_TYPE_TORRENT_BE_DOWNLOADED => ['text' => 'Torrent be downloaded'], self::BUSINESS_TYPE_RECEIVE_REWARD => ['text' => 'Receive reward'], self::BUSINESS_TYPE_RECEIVE_GIFT => ['text' => 'Receive gift'], + self::BUSINESS_TYPE_UPLOAD_TORRENT => ['text' => 'Upload torrent'], ]; public function getBusinessTypeTextAttribute() diff --git a/app/Models/PersonalAccessTokenPlain.php b/app/Models/PersonalAccessTokenPlain.php deleted file mode 100644 index 8c66a5d7..00000000 --- a/app/Models/PersonalAccessTokenPlain.php +++ /dev/null @@ -1,11 +0,0 @@ -extra[self::EXTRA_TAXONOMY_LABELS] ?? [] as $item) { if ($item['torrent_field'] == $torrentField) { - return $item['display_text'][$lang] ?? 'Unknown'; + if (!empty($item['display_text'][$lang])) { + return $item['display_text'][$lang]; + } } } return nexus_trans("searchbox.sub_category_{$torrentField}_label") ?: ucfirst($torrentField); @@ -214,7 +217,7 @@ class SearchBox extends NexusModel public static function isSpecialEnabled(): bool { - return Setting::get('main.spsct') == 'yes'; + return Setting::getIsSpecialSectionEnabled(); } public static function getBrowseMode() @@ -303,7 +306,12 @@ class SearchBox extends NexusModel public function loadTags(): void { - $this->setRelation("tags", TagRepository::listAll($this->getKey())); + $allTags = TagRepository::listAll($this->getKey()); + if (!Permission::canSetTorrentSpecialTag()) { + $specialTagIdList = Tag::listSpecial(); + $allTags = $allTags->filter(fn ($item) => !in_array($item->id, $specialTagIdList)); + } + $this->setRelation("tags", $allTags); } public static function getDefaultSearchMode() @@ -349,5 +357,4 @@ class SearchBox extends NexusModel return $results; } - } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index c2b849a8..ae0e3beb 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -100,7 +100,7 @@ class Setting extends NexusModel return $value; } - public static function getDefaultLang() + public static function getDefaultLang(): string { return self::get("main.defaultlang"); } @@ -110,4 +110,112 @@ class Setting extends NexusModel return self::get("security.use_challenge_response_authentication") == "yes"; } + public static function getUploadTorrentMaxSize(): int + { + return intval(self::get("main.max_torrent_size")); + } + + public static function getUploadTorrentMaxPrice(): int + { + return intval(self::get("torrent.max_price")); + } + + public static function getIsPaidTorrentEnabled(): bool + { + return self::get("torrent.paid_torrent_enabled") == "yes"; + } + + public static function getUploadDenyApprovalDenyCount(): int + { + return intval(self::get("main.upload_deny_approval_deny_count")); + } + + public static function getOfferSkipApprovedCount(): int + { + return intval(self::get("main.offer_skip_approved_count")); + } + + public static function getLargeTorrentSize(): int + { + return intval(self::get("torrent.largesize")); + } + + public static function getLargeTorrentSpState(): int + { + return intval(self::get("torrent.largepro")); + } + + public static function getUploadTorrentHalfDownProbability(): int + { + return intval(self::get("torrent.randomhalfleech")); + } + + public static function getUploadTorrentFreeProbability(): int + { + return intval(self::get("torrent.randomfree")); + } + + public static function getUploadTorrentTwoTimesUpProbability(): int + { + return intval(self::get("torrent.randomtwoup")); + } + + public static function getUploadTorrentFreeTwoTimesUpProbability(): int + { + return intval(self::get("torrent.randomtwoupfree")); + } + + public static function getUploadTorrentHalfDownTwoTimesUpProbability(): int + { + return intval(self::get("torrent.randomtwouphalfdown")); + } + + public static function getUploadTorrentOneThirdDownProbability(): int + { + return intval(self::get("torrent.randomthirtypercentdown")); + } + + public static function getUploadTorrentRewardBonus(): int + { + return intval(self::get("bonus.uploadtorrent")); + } + + + + public static function getIsUploadOpenAtWeekend(): bool + { + return self::get("main.sptime") == "yes"; + } + + public static function getIsSpecialSectionEnabled(): bool + { + return self::get('main.spsct') == 'yes'; + } + + public static function getIsAllowUserReceiveEmailNotification(): bool + { + return self::get('smtp.emailnotify') == 'yes'; + } + + public static function getBaseUrl(): string + { + $result = self::get('basic.BASEURL', $_SERVER['HTTP_HOST'] ?? ''); + return rtrim($result, '/'); + } + + public static function getSiteName(): string + { + return self::get("basic.SITENAME"); + } + + public static function getTorrentSaveDir(): string + { + return self::get("main.torrent_dir"); + } + + public static function getSmtpType(): string + { + return self::get("smtp.smtptype"); + } + } diff --git a/app/Models/SiteLog.php b/app/Models/SiteLog.php new file mode 100644 index 00000000..96e40979 --- /dev/null +++ b/app/Models/SiteLog.php @@ -0,0 +1,12 @@ + 'datetime', 'promotion_until' => 'datetime', 'pos_state_until' => 'datetime', + 'last_action' => 'datetime', ]; public static $commentFields = [ @@ -334,15 +336,6 @@ class Torrent extends NexusModel return implode('', $html); } - public static function getBasicInfo(): array - { - $result = []; - foreach (self::$basicRelations as $relation) { - $result[$relation] = nexus_trans("torrent.show.$relation"); - } - return $result; - } - public static function listPosStates($onlyKeyValue = false, $valueField = 'text'): array { $result = self::$posStates; @@ -382,6 +375,11 @@ class Torrent extends NexusModel return true; } + public function getSubCategoryLabel($field): string + { + return $this->basic_category->search_box->getTaxonomyLabel($field); + } + public function bookmarks(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(Bookmark::class, 'torrentid'); @@ -452,7 +450,7 @@ class Torrent extends NexusModel return $this->belongsTo(Source::class, 'source'); } - public function basic_media() + public function basic_medium() { return $this->belongsTo(Media::class, 'medium'); } @@ -477,11 +475,21 @@ class Torrent extends NexusModel return $this->belongsTo(Team::class, 'team'); } - public function basic_audio_codec() + public function basic_audiocodec() { return $this->belongsTo(AudioCodec::class, 'audiocodec'); } + public function claim_users(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany(User::class, 'claims', 'torrent_id'); + } + + public function claims() + { + return $this->hasMany(Claim::class, 'torrent_id'); + } + public function scopeVisible($query, $visible = self::VISIBLE_YES) { $query->where('visible', $visible); diff --git a/app/Models/TorrentExtra.php b/app/Models/TorrentExtra.php index 98358f56..4bf9d80f 100644 --- a/app/Models/TorrentExtra.php +++ b/app/Models/TorrentExtra.php @@ -3,16 +3,25 @@ namespace App\Models; use Nexus\Database\NexusDB; +use Nexus\Torrent\TechnicalInformation; class TorrentExtra extends NexusModel { public $timestamps = true; - protected $fillable = ['torrent_id', 'descr', 'ori_descr', 'media_info']; + protected $fillable = ['torrent_id', 'descr', 'ori_descr', 'media_info', 'nfo']; public function torrent() { return $this->belongsTo(Torrent::class, 'torrent_id'); } + protected $appends = ['media_info_summary']; + + public function getMediaInfoSummaryAttribute(): array + { + $technicalInfo = new TechnicalInformation($this->media_info ?? ''); + return $technicalInfo->getSummaryInfo(); + } + } diff --git a/app/Models/User.php b/app/Models/User.php index 1a5e0e00..17d1ce64 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -540,6 +540,11 @@ class User extends Authenticatable implements FilamentUser, HasName return $this->hasMany(UserModifyLog::class, "user_id"); } + public function claims(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Claim::class, 'uid'); + } + public function getAvatarAttribute($value) { if ($value) { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 6b6e599a..5d649103 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -6,13 +6,15 @@ use App\Events\SeedBoxRecordUpdated; use App\Events\TorrentCreated; use App\Events\TorrentDeleted; use App\Events\TorrentUpdated; -use App\Events\UserDestroyed; +use App\Events\UserDeleted; use App\Events\UserDisabled; use App\Listeners\DeductUserBonusWhenTorrentDeleted; use App\Listeners\FetchTorrentImdb; use App\Listeners\RemoveOauthTokens; use App\Listeners\RemoveSeedBoxRecordCache; -use App\Listeners\SyncTorrentToEs; +use App\Listeners\SendEmailNotificationWhenTorrentCreated; +use App\Listeners\SyncTorrentToElasticsearch; +use App\Listeners\SyncTorrentToMeilisearch; use App\Listeners\TestTorrentUpdated; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; @@ -34,11 +36,14 @@ class EventServiceProvider extends ServiceProvider RemoveSeedBoxRecordCache::class, ], TorrentUpdated::class => [ - SyncTorrentToEs::class, - TestTorrentUpdated::class, + SyncTorrentToElasticsearch::class, + SyncTorrentToMeilisearch::class, ], TorrentCreated::class => [ FetchTorrentImdb::class, + SyncTorrentToElasticsearch::class, + SyncTorrentToMeilisearch::class, + SendEmailNotificationWhenTorrentCreated::class, ], TorrentDeleted::class => [ DeductUserBonusWhenTorrentDeleted::class, diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 68736800..9a37752a 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -6,6 +6,7 @@ use App\Models\Setting; use App\Models\Torrent; use App\Models\User; use Illuminate\Encryption\Encrypter; +use Illuminate\Http\Request; use Illuminate\Support\Str; class BaseRepository @@ -20,6 +21,11 @@ class BaseRepository return [$field, $type]; } + protected function getPerPageFromRequest(Request $request) + { + return $request->get('per_page'); + } + protected function handleAnonymous($username, $user, User $authenticator, Torrent $torrent = null) { if (!$user) { diff --git a/app/Repositories/TokenRepository.php b/app/Repositories/TokenRepository.php index 2daf0ed8..6fbd41a8 100644 --- a/app/Repositories/TokenRepository.php +++ b/app/Repositories/TokenRepository.php @@ -7,7 +7,9 @@ class TokenRepository extends BaseRepository { private static array $userTokenPermissions = [ PermissionEnum::TORRENT_LIST, + PermissionEnum::TORRENT_VIEW, PermissionEnum::UPLOAD, + PermissionEnum::USER_VIEW, ]; public function listUserTokenPermissions(): array diff --git a/app/Repositories/ToolRepository.php b/app/Repositories/ToolRepository.php index bdbe5322..7bf461d2 100644 --- a/app/Repositories/ToolRepository.php +++ b/app/Repositories/ToolRepository.php @@ -346,7 +346,8 @@ class ToolRepository extends BaseRepository ->from(new Address(Setting::get('main.SITEEMAIL'), Setting::get('basic.SITENAME'))) ->to($to) ->subject($subject) - ->html($body) + ->text($body) + ->html(nl2br($body)) ; // Send the message diff --git a/app/Repositories/TorrentRepository.php b/app/Repositories/TorrentRepository.php index a71374df..0c2d861d 100644 --- a/app/Repositories/TorrentRepository.php +++ b/app/Repositories/TorrentRepository.php @@ -2,8 +2,10 @@ namespace App\Repositories; +use App\Auth\Permission; use App\Exceptions\InsufficientPermissionException; use App\Exceptions\NexusException; +use App\Http\Resources\TorrentResource; use App\Models\AudioCodec; use App\Models\Category; use App\Models\Claim; @@ -26,9 +28,12 @@ use App\Models\TorrentOperationLog; use App\Models\TorrentSecret; use App\Models\TorrentTag; use App\Models\User; +use App\Utils\ApiQueryBuilder; use Carbon\Carbon; -use Hashids\Hashids; +use Elasticsearch\Endpoints\Search; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -55,81 +60,134 @@ class TorrentRepository extends BaseRepository /** * fetch torrent list - * - * @param array $params - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ - public function getList(array $params, User $user) + public function getList(Request $request, Authenticatable $user, string $sectionName = null) { - $query = Torrent::query(); - if (!empty($params['category'])) { - $query->where('category', $params['category']); + if (empty($sectionName)) { + $sectionId = SearchBox::getBrowseMode(); + $searchBox = SearchBox::query()->find($sectionId); + } else { + $searchBox = SearchBox::query()->where('name', $sectionName)->first(); } - if (!empty($params['source'])) { - $query->where('source', $params['source']); + if (empty($searchBox)) { + throw new NexusException(nexus_trans("upload.invalid_section")); } - if (!empty($params['medium'])) { - $query->where('medium', $params['medium']); + if ($searchBox->isSectionSpecial() && !Permission::canViewSpecialSection()) { + throw new InsufficientPermissionException(); } - if (!empty($params['codec'])) { - $query->where('codec', $params['codec']); + $categoryIdList = $searchBox->categories()->pluck('id')->toArray(); + //query this info default + $query = Torrent::query()->with([ + 'basic_category', 'basic_category.search_box', + 'basic_audiocodec', 'basic_codec', 'basic_medium', + 'basic_source', 'basic_processing', 'basic_standard', 'basic_team', + ]) + ->whereIn('category', $categoryIdList) + ->orderBy("pos_state", "DESC"); + $allowIncludes = ['user', 'extra', 'tags']; + $allowIncludeCounts = ['thank_users', 'reward_logs', 'claims']; + $allowIncludeFields = [ + 'has_bookmarked', 'has_claimed', 'has_thanked', 'has_rewarded', + 'description', 'download_url' + ]; + $allowFilters = [ + 'title', 'category', 'source', 'medium', 'codec', 'audiocodec', 'standard', 'processing', 'team', + 'owner', 'visible', 'added', 'size', 'sp_state', 'leechers', 'seeders', 'times_completed' + ]; + $allowSorts = ['id', 'comments', 'size', 'seeders', 'leechers', 'times_completed']; + $apiQueryBuilder = ApiQueryBuilder::for(TorrentResource::NAME, $query, $request) + ->allowIncludes($allowIncludes) + ->allowIncludeCounts($allowIncludeCounts) + ->allowIncludeFields($allowIncludeFields) + ->allowFilters($allowFilters) + ->allowSorts($allowSorts) + ->registerCustomFilter('title', function (Builder $query, Request $request) { + $title = $request->input(ApiQueryBuilder::PARAM_NAME_FILTER.".title"); + if ($title) { + $query->where(function (Builder $query) use ($title) { + $query->where('name', 'like', '%' . $title . '%') + ->orWhere('small_descr', 'like', '%' . $title . '%'); + }); + } + }) + ; + $query = $apiQueryBuilder->build(); + if (!$apiQueryBuilder->hasSort()) { + $query->orderBy("id", "DESC"); } - if (!empty($params['audio_codec'])) { - $query->where('audiocodec', $params['audio_codec']); - } - if (!empty($params['standard'])) { - $query->where('standard', $params['standard']); - } - if (!empty($params['processing'])) { - $query->where('processing', $params['processing']); - } - if (!empty($params['team'])) { - $query->where('team', $params['team']); - } - if (!empty($params['owner'])) { - $query->where('owner', $params['owner']); - } - if (!empty($params['visible'])) { - $query->where('visible', $params['visible']); - } - - if (!empty($params['query'])) { - $query->where(function (Builder $query) use ($params) { - $query->where('name', 'like', "%{$params['query']}%") - ->orWhere('small_descr', 'like', "%{$params['query']}%"); - }); - } - - if (!empty($params['category_mode'])) { - $query->whereHas('basic_category', function (Builder $query) use ($params) { - $query->where('mode', $params['category_mode']); - }); - } - - $query = $this->handleGetListSort($query, $params); - - $with = ['user', 'tags']; - $torrents = $query->with($with)->paginate(); - foreach ($torrents as &$item) { - $item->download_url = $this->getDownloadUrl($item->id, $user); - } - return $torrents; + $torrents = $query->paginate($this->getPerPageFromRequest($request)); + return $this->appendIncludeFields($apiQueryBuilder, $user, $torrents); } - public function getDetail($id, User $user) + public function getDetail($id, Authenticatable $user) { - $with = [ - 'user', 'basic_audio_codec', 'basic_category', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team', - 'thanks' => function ($query) use ($user) { - $query->where('userid', $user->id); - }, - 'reward_logs' => function ($query) use ($user) { - $query->where('userid', $user->id); - }, + //query this info default + $query = Torrent::query()->with([ + 'basic_category', 'basic_category.search_box', + 'basic_audiocodec', 'basic_codec', 'basic_medium', + 'basic_source', 'basic_processing', 'basic_standard', 'basic_team', + ]); + $allowIncludes = ['user', 'extra', 'tags']; + $allowIncludeCounts = ['thank_users', 'reward_logs', 'claims']; + $allowIncludeFields = [ + 'has_bookmarked', 'has_claimed', 'has_thanked', 'has_rewarded', + 'description', 'download_url' ]; - $result = Torrent::query()->with($with)->withCount(['peers', 'thank_users', 'reward_logs'])->visible()->findOrFail($id); - $result->download_url = $this->getDownloadUrl($id, $user); - return $result; + $apiQueryBuilder = ApiQueryBuilder::for(TorrentResource::NAME, $query) + ->allowIncludes($allowIncludes) + ->allowIncludeCounts($allowIncludeCounts) + ->allowIncludeFields($allowIncludeFields) + ; + $torrent = $apiQueryBuilder->build()->findOrFail($id); + $torrentList = $this->appendIncludeFields($apiQueryBuilder, $user, [$torrent]); + return $torrentList[0]; + } + + private function appendIncludeFields(ApiQueryBuilder $apiQueryBuilder, Authenticatable $user, $torrentList) + { + $torrentIdArr = $bookmarkData = $claimData = $thankData = $rewardData =[]; + foreach ($torrentList as $torrent) { + $torrentIdArr[] = $torrent->id; + } + unset($torrent); + if ($hasFieldHasBookmarked = $apiQueryBuilder->hasIncludeField('has_bookmarked')) { + $bookmarkData = $user->bookmarks()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); + } + if ($hasFieldHasClaimed = $apiQueryBuilder->hasIncludeField('has_claimed')) { + $claimData = $user->claims()->whereIn('torrent_id', $torrentIdArr)->get()->keyBy('torrent_id'); + } + if ($hasFieldHasThanked = $apiQueryBuilder->hasIncludeField('has_thanked')) { + $thankData = $user->thank_torrent_logs()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); + } + if ($hasFieldHasRewarded = $apiQueryBuilder->hasIncludeField('has_rewarded')) { + $rewardData = $user->reward_torrent_logs()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); + } + + foreach ($torrentList as $torrent) { + $id = $torrent->id; + if ($hasFieldHasBookmarked) { + $torrent->has_bookmarked = $bookmarkData->has($id); + } + if ($hasFieldHasClaimed) { + $torrent->has_claimed = $claimData->has($id); + } + if ($hasFieldHasThanked) { + $torrent->has_thanked = $thankData->has($id); + } + if ($hasFieldHasRewarded) { + $torrent->has_rewarded = $rewardData->has($id); + } + + if ($apiQueryBuilder->hasIncludeField('description') && $apiQueryBuilder->hasInclude('extra')) { + $descriptionArr = format_description($torrent->extra->descr ?? ''); + $torrent->description = $descriptionArr; + $torrent->images = get_image_from_description($descriptionArr); + } + if ($apiQueryBuilder->hasIncludeField("download_url")) { + $torrent->download_url = $this->getDownloadUrl($id, $user); + } + } + return $torrentList; } public function getDownloadUrl($id, array|User $user): string diff --git a/app/Repositories/UploadRepository.php b/app/Repositories/UploadRepository.php index 82b11efb..072a868d 100644 --- a/app/Repositories/UploadRepository.php +++ b/app/Repositories/UploadRepository.php @@ -1,52 +1,195 @@ user(); - if ($user->uploadpos != 'yes') { - throw new NexusException("user upload permission is disabled"); + if (empty($request->name)) { + throw new NexusException(nexus_trans("upload.require_name")); } - $rules = [ - 'descr' => 'required', - 'type' => 'required', - 'name' => 'required', - ]; - $request->validate($rules); - $category = Category::query()->findOrFail($request->type); - $mode = $category->mode; - $searchBox = SearchBox::query()->findOrFail($mode); - $searchBox->loadSubCategories(); - $searchBox->loadTags(); + if (empty($request->descr)) { + throw new NexusException(nexus_trans("upload.blank_description")); + } + if (empty($request->type)) { + throw new NexusException(nexus_trans("upload.category_unselected")); + } + $category = Category::query()->find($request->type); + if (!$category) { + throw new NexusException(nexus_trans("upload.invalid_category")); + } + $torrentFile = $this->getTorrentFile($request); + $filepath = $torrentFile->getRealPath(); + try { + $dict = Bencode::load($filepath); + } catch (ParseException $e) { + do_log("Bencode load error:" . $e->getMessage(), 'error'); + throw new NexusException("upload.not_bencoded_file"); + } + $info = $this->checkTorrentDict($dict, 'info'); + if (isset($dict['piece layers']) || isset($info['files tree']) || (isset($info['meta version']) && $info['meta version'] == 2)) { + throw new NexusException("Torrent files created with Bittorrent Protocol v2, or hybrid torrents are not supported."); + } + $this->checkTorrentDict($info, 'piece length', 'integer'); // Only Check without use + $dname = $this->checkTorrentDict($info, 'name', 'string'); + $pieces = $this->checkTorrentDict($info, 'pieces', 'string'); + if (strlen($pieces) % 20 != 0) { + throw new NexusException(nexus_trans("upload.invalid_pieces")); + } + $dict['info']['private'] = 1; + $dict['info']['source'] = sprintf("[%s] %s", Setting::getBaseUrl(), Setting::getSiteName()); + unset ($dict['announce-list']); // remove multi-tracker capability + unset ($dict['nodes']); // remove cached peers (Bitcomet & Azareus) + $infoHash = pack("H*", sha1(Bencode::encode($dict['info']))); + $exists = Torrent::query()->where('info_hash', $infoHash)->first(['id']); + if ($exists) { + throw new NexusException(nexus_trans('upload.torrent_existed', ['id' => $exists->id])); + } + $subCategoriesAngTags = $this->getSubCategoriesAndTags($request, $category); + $fileListInfo = $this->getFileListInfo($info, $dname); + $posStateInfo = $this->getPosStateInfo($request); + $pickInfo = $this->getPickInfo($request); $anonymous = "no"; $uploaderUsername = $user->username; - if ($request->uplver == 'yes' && Permission::canBeAnonymous()) { + if ($request->uplver == 'yes') { + if (!Permission::canBeAnonymous()) { + throw new NexusException(nexus_trans('upload.no_permission_to_be_anonymous')); + } $anonymous = "yes"; $uploaderUsername = "Anonymous"; } - - - + $torrentSavePath = $this->getTorrentSavePath(); + $nowStr = Carbon::now()->toDateTimeString(); + $torrentInsert = [ + 'filename' => $torrentFile->getClientOriginalName(), + 'owner' => $user->id, + 'visible' => 'yes', + 'anonymous' => $anonymous, + 'name' => $request->name, + 'size' => $fileListInfo['totalLength'], + 'numfiles' => count($fileListInfo['fileList']), + 'type' => $fileListInfo['type'], + 'url' => parse_imdb_id($request->url ?? ''), + 'small_descr' => $request->small_descr ?? '', + 'category' => $category->id, + 'source' => $subCategoriesAngTags['subCategories']['source'], + 'medium' => $subCategoriesAngTags['subCategories']['medium'], + 'codec' => $subCategoriesAngTags['subCategories']['codec'], + 'audiocodec' => $subCategoriesAngTags['subCategories']['audiocodec'], + 'standard' => $subCategoriesAngTags['subCategories']['standard'], + 'processing' => $subCategoriesAngTags['subCategories']['processing'], + 'team' => $subCategoriesAngTags['subCategories']['team'], + 'save_as' => $dname, + 'sp_state' => $this->getSpState($fileListInfo['totalLength']), + 'added' => $nowStr, + 'last_action' => $nowStr, + 'info_hash' => $infoHash, + 'cover' => $this->getCover($request), + 'pieces_hash' => sha1($info['pieces']), + 'cache_stamp' => time(), + 'hr' => $this->getHitAndRun($request), + 'pos_state' => $posStateInfo['posState'], + 'pos_state_until' => $posStateInfo['posStateUntil'], + 'picktype' => $pickInfo['pickType'], + 'picktime' => $pickInfo['pickTime'], + 'approval_status' => $this->getApprovalStatus($request), + 'price' => $this->getPrice($request), + ]; + $extraInsert = [ + 'descr' => $request->descr ?? '', + 'media_info' => $request->technical_info ?? '', + 'nfo' => $this->getNfoContent($request), + 'created_at' => $nowStr, + ]; + $newTorrent = DB::transaction(function () use ($torrentInsert, $extraInsert, $fileListInfo, $subCategoriesAngTags, $dict, $torrentSavePath) { + $newTorrent = Torrent::query()->create($torrentInsert); + $id = $newTorrent->id; + $torrentFilePath = "$torrentSavePath/$id.torrent"; + $saveResult = Bencode::dump($torrentFilePath, $dict); + if ($saveResult === false) { + do_log("save torrent failed: $torrentFilePath", 'error'); + throw new NexusException(nexus_trans('upload.save_torrent_file_failed')); + } + $extraInsert['torrent_id'] = $id; + TorrentExtra::query()->insert($extraInsert); + $fileInsert = []; + foreach ($fileListInfo['fileList'] as $fileItem) { + $fileInsert[] = [ + 'torrent' => $id, + 'filename' => $fileItem[0], + 'size' => $fileItem[1], + ]; + } + File::query()->insert($fileInsert); + if (!empty($subCategoriesAngTags['tags'])) { + insert_torrent_tags($id, $subCategoriesAngTags['tags']); + } + $this->sendReward($id); + return $newTorrent; + }); + $id = $newTorrent->id; + $torrentRep = new TorrentRepository(); + $torrentRep->addPiecesHashCache($id, $torrentInsert['pieces_hash']); + write_log("Torrent $id ($newTorrent->name) was uploaded by $uploaderUsername"); + fire_event(ModelEventEnum::TORRENT_CREATED, $newTorrent); + return $newTorrent; } private function getTorrentFile(Request $request): UploadedFile { $file = $request->file('file'); if (empty($file)) { - throw new NexusException("torrent file not found"); + throw new NexusException(nexus_trans('upload.missing_torrent_file')); } if (!$file->isValid()) { throw new NexusException("upload torrent file error"); } + $size = $file->getSize(); + $maxAllowSize = Setting::getUploadTorrentMaxSize(); + if ($size > $maxAllowSize) { + $msg = sprintf("%s%s%s", + nexus_trans("upload.torrent_file_too_big"), + number_format($maxAllowSize), + nexus_trans("upload.remake_torrent_note") + ); + throw new NexusException($msg); + } + if ($size == 0) { + throw new NexusException("upload.empty_file"); + } + $filename = $file->getClientOriginalName(); + if (!validfilename($filename)) { + throw new NexusException("upload.invalid_filename"); + } + if (!preg_match('/^(.+)\.torrent$/si', $filename, $matches)) { + throw new NexusException("upload.filename_not_torrent"); + } return $file; } @@ -61,16 +204,387 @@ class UploadRepository extends BaseRepository return ''; } if (!$file->isValid()) { - throw new NexusException("upload nfo file error"); + throw new NexusException(nexus_trans("upload.nfo_upload_failed")); } $size = $file->getSize(); if ($size == 0) { - throw new NexusException("upload nfo file size is zero"); + throw new NexusException(nexus_trans("upload.zero_byte_nfo")); } if ($size > 65535) { - throw new NexusException("upload nfo file size is too large"); + throw new NexusException(nexus_trans("upload.nfo_too_big")); } return str_replace("\x0d\x0d\x0a", "\x0d\x0a", $file->getContent()); } + private function getApprovalStatus(Request $request): int + { + if (Permission::canTorrentApprovalAllowAutomatic()) { + return Torrent::APPROVAL_STATUS_ALLOW; + } + return Torrent::APPROVAL_STATUS_NONE; + } + + private function getPrice(Request $request): int + { + $price = $request->price ?: 0; + if (!is_numeric($price)) { + throw new NexusException(nexus_trans('upload.invalid_price', ['price' => $price])); + } + if ($price > 0) { + if (!Permission::canSetTorrentPrice()) { + throw new NexusException(nexus_trans("upload.no_permission_to_set_torrent_price")); + } + $paidTorrentEnabled = Setting::getIsPaidTorrentEnabled(); + if (!$paidTorrentEnabled) { + throw new NexusException(nexus_trans("upload.paid_torrent_not_enabled")); + } + $maxPrice = Setting::getUploadTorrentMaxPrice(); + if ($maxPrice > 0 && $price > $maxPrice) { + throw new NexusException(nexus_trans('upload.price_too_much')); + } + } + return intval($price); + } + + private function getHitAndRun(Request $request): int + { + $hr = $request->hr ?? 0; + if ($hr > 0 && !Permission::canSetTorrentHitAndRun()) { + throw new NexusException(nexus_trans("upload.no_permission_to_set_torrent_hr")); + } + if (!in_array($hr, [0, 1])) { + throw new NexusException(nexus_trans('upload.invalid_hr')); + } + return intval($hr); + } + + private function getPosStateInfo(Request $request): array + { + $posState = $request->pos_state ?: Torrent::POS_STATE_STICKY_NONE; + $posStateUntil = $request->pos_state_until ?: null; + if ($posState !== Torrent::POS_STATE_STICKY_NONE) { + if (!Permission::canSetTorrentPosState()) { + throw new NexusException("upload.no_permission_to_set_torrent_pos_state"); + } + if (!isset(Torrent::$posStates[$posState])) { + throw new NexusException(nexus_trans('upload.invalid_pos_state', ['pos_state' => $posState])); + } + } + if ($posState == Torrent::POS_STATE_STICKY_NONE) { + $posStateUntil = null; + } + if ($posStateUntil && Carbon::parse($posStateUntil)->lt(Carbon::now())) { + throw new NexusException(nexus_trans('upload.invalid_pos_state_until')); + } + return compact('posState', 'posStateUntil'); + } + + private function getPickInfo(Request $request): array + { + $pickType = $request->pick_type ?: Torrent::PICK_NORMAL; + $pickTime = null; + if ($pickType != Torrent::PICK_NORMAL) { + if (!isset(Torrent::$pickTypes[$pickType])) { + throw new NexusException(nexus_trans('upload.invalid_pick_type', ['pick_type' => $pickType])); + } + if (!Permission::canPickTorrent()) { + throw new NexusException("upload.no_permission_to_pick_torrent"); + } + $pickTime = Carbon::now(); + } + return compact('pickType', 'pickTime'); + } + + private function checkTorrentDict($dict, $key, $type = null) + { + if (!is_array($dict)) { + throw new NexusException(nexus_trans("upload.not_a_dictionary")); + } + if (!isset($dict[$key])) { + throw new NexusException(nexus_trans("upload.dictionary_is_missing_key")); + } + $value = $dict[$key]; + if (!is_null($type)) { + $isFunction = 'is_' . $type; + if (function_exists($isFunction) && !$isFunction($value)) { + throw new NexusException(nexus_trans("upload.invalid_entry_in_dictionary")); + } + } + return $value; + } + + /** + * @throws NexusException + */ + private function getFileListInfo(array $info, string $dname): array + { + $filelist = array(); + $totallen = 0; + if (isset($info['length'])) { + $totallen = $info['length']; + $filelist[] = array($dname, $totallen); + $type = "single"; + } else { + $flist = $this->checkTorrentDict($info, 'files', 'array'); + + if (!count($flist)) { + throw new NexusException(nexus_trans("upload.empty_file")); + } + foreach ($flist as $fn) { + $ll = $this->checkTorrentDict($fn, 'length', 'integer'); + $path_key = isset($fn['path.utf-8']) ? 'path.utf-8' : 'path'; + $ff = $this->checkTorrentDict($fn, $path_key, 'list'); + + $totallen += $ll; + $ffa = array(); + foreach ($ff as $ffe) { + if (!is_string($ffe)) { + throw new NexusException(nexus_trans("upload.filename_errors")); + } + $ffa[] = $ffe; + } + + if (!count($ffa)) { + throw new NexusException(nexus_trans("upload.filename_errors")); + } + $ffe = implode("/", $ffa); + $filelist[] = array($ffe, $ll); + } + $type = "multi"; + } + return [ + 'type' => $type, + 'totalLength' => $totallen, + 'fileList' => $filelist, + ]; + } + + private function canUploadToSection(SearchBox $section): bool + { + $user = Auth::user(); + $uploadDenyApprovalDenyCount = Setting::getUploadDenyApprovalDenyCount(); + $approvalDenyCount = Torrent::query()->where('owner', $user->id) + ->where('approval_status', Torrent::APPROVAL_STATUS_DENY) + ->count() + ; + if ($uploadDenyApprovalDenyCount > 0 && $approvalDenyCount >= $uploadDenyApprovalDenyCount) { + throw new NexusException(nexus_trans("upload.approval_deny_reach_upper_limit")); + } + if ($section->isSectionBrowse()) { + $offerSkipApprovedCount = Setting::getOfferSkipApprovedCount(); + if ($user->offer_allowed_count >= $offerSkipApprovedCount) { + return true; + } + if (get_if_restricted_is_open()) { + return true; + } + if (!Permission::canUploadToNormalSection()) { + throw new NexusException(nexus_trans('upload.unauthorized_upload_freely')); + } + return true; + } elseif ($section->isSectionSpecial()) { + if (!Setting::getIsSpecialSectionEnabled()) { + throw new NexusException(nexus_trans('upload.special_section_not_enabled')); + } + if (!Permission::canUploadToSpecialSection()) { + throw new NexusException(nexus_trans('upload.unauthorized_upload_freely')); + } + return true; + } + throw new NexusException(nexus_trans('upload.invalid_section')); + } + + private function getSpState($torrentSize): int + { + $largeTorrentSize = Setting::getLargeTorrentSize(); + if ($largeTorrentSize > 0 && $torrentSize > $largeTorrentSize * 1073741824) { + $largeTorrentSpState = Setting::getLargeTorrentSpState(); + if (isset(Torrent::$promotionTypes[$largeTorrentSpState])) { + do_log("large torrent, sp state from config: $largeTorrentSpState"); + return $largeTorrentSpState; + } + do_log("invalid large torrent sp state: $largeTorrentSpState", 'error'); + return Torrent::PROMOTION_NORMAL; + } else { + $probabilities = [ + Torrent::PROMOTION_FREE => Setting::getUploadTorrentFreeProbability(), + Torrent::PROMOTION_TWO_TIMES_UP => Setting::getUploadTorrentTwoTimesUpProbability(), + Torrent::PROMOTION_FREE_TWO_TIMES_UP => Setting::getUploadTorrentFreeTwoTimesUpProbability(), + Torrent::PROMOTION_HALF_DOWN => Setting::getUploadTorrentHalfDownProbability(), + Torrent::PROMOTION_HALF_DOWN_TWO_TIMES_UP => Setting::getUploadTorrentHalfDownTwoTimesUpProbability(), + Torrent::PROMOTION_ONE_THIRD_DOWN => Setting::getUploadTorrentOneThirdDownProbability(), + ]; + $sum = array_sum($probabilities); + if ($sum == 0) { + do_log("no random sp state", 'warning'); + return Torrent::PROMOTION_NORMAL; + } + $random = mt_rand(1, $sum); + $currentProbability = 0; + foreach ($probabilities as $k => $v) { + $currentProbability += $v; + if ($random <= $currentProbability) { + do_log(sprintf("random sp state, probabilities: %s, get result: %s by probability: %s", json_encode($probabilities), $k, $v)); + return $k; + } + } + throw new \RuntimeException(); + } + } + + /** + * @throws NexusException + */ + private function getSubCategoriesAndTags(Request $request, Category $category): array + { + $searchBoxRep = new SearchBoxRepository(); + $sections = $searchBoxRep->listSections()->keyBy('id'); + if (!$sections->has($category->mode)) { + throw new NexusException(nexus_trans('upload.invalid_section')); + } + /** + * @var $section SearchBox + */ + $section = $sections->get($category->mode); + $this->canUploadToSection($section); + + $sectionResource = new SearchBoxResource($section); + $sectionData = $sectionResource->response()->getData(true); + $sectionInfo = $sectionData['data']; + $categories = array_column($sectionInfo['categories'], 'id'); + if (!in_array($category->id, $categories)) { + throw new NexusException(nexus_trans('upload.invalid_category')); + } + $subCategoryInfo = array_column($sectionInfo['sub_categories'], null, 'field'); + $subCategories = []; + foreach (SearchBox::$taxonomies as $name => $info) { + $value = $request->get($name, 0); + if ($value > 0) { + if (!isset($subCategoryInfo[$name])) { + throw new NexusException(nexus_trans('upload.not_supported_sub_category_field', ['field' => $name])); + } + $subCategoryValues = array_column($subCategoryInfo[$name]['data'], 'name', 'id'); + if (!isset($subCategoryValues[$value])) { + throw new NexusException(nexus_trans( + 'upload.invalid_sub_category_value', + ['field' => $name, 'label' => $subCategoryInfo[$name]['label'], 'value' => $value] + )); + } + } + $subCategories[$name] = $value; + } + $tags = $request->tags ?: []; + if (!is_array($tags)) { + $tags = explode(',', $tags); + } + $allTags = array_column($sectionInfo['tags'], 'name', 'id'); + foreach ($tags as $tag) { + if (!isset($allTags[$tag])) { + throw new NexusException(nexus_trans('upload.invalid_tag', ['tag' => $tag])); + } + } + return compact('subCategories', 'tags'); + } + + private function getCover(Request $request):string + { + $descr = $request->descr ?? ''; + if (empty($descr)) { + return ''; + } + $descriptionArr = format_description($descr); + return get_image_from_description($descriptionArr, true, false); + } + + private function getTorrentSavePath(): string + { + $torrentSavePath = getFullDirectory(Setting::getTorrentSaveDir()); + if (!is_dir($torrentSavePath)) { + do_log(sprintf("torrentSavePath: %s not exists", $torrentSavePath), 'error'); + throw new NexusException(nexus_trans('upload.torrent_save_dir_not_exists')); + } + if (!is_writable($torrentSavePath)) { + do_log(sprintf("torrentSavePath: %s not writable", $torrentSavePath), 'error'); + throw new NexusException(nexus_trans('upload.torrent_save_dir_not_writable')); + } + return $torrentSavePath; + } + + private function sendReward($torrentId): void + { + $user = Auth::user(); + $old = $user->seedbonus; + $delta = Setting::getUploadTorrentRewardBonus(); + if ($delta > 0) { + $new = $old + $delta; + $user->increment('seedbonus', $delta); + BonusLogs::add($user->id, $old, $delta, $new, "Upload torrent: $torrentId", BonusLogs::BUSINESS_TYPE_UPLOAD_TORRENT); + do_log("upload torrent: $torrentId, success send reward: $delta"); + } else { + do_log("upload torrent: $torrentId, no reward"); + } + } + + public function sendEmailNotification(Torrent $torrent, $userId = 0): int + { + $logMsg = sprintf("torrent: %s, category: %s", $torrent->id, $torrent->category); + if (!Setting::getIsAllowUserReceiveEmailNotification() || Setting::getSmtpType() == 'none') { + do_log("$logMsg, not allow user receive email notification or smtp type is none"); + return 0; + } + $page = 1; + $size = 1000; + $query = User::query() + ->where("notifs", "like", "%[cat$torrent->category]%") + ->where("notifs", "like","%[email]%") + ->normal() + ; + if ($userId > 0) { + $query->where("id", $userId); + } + $total = (clone $query)->count(); + if ($total == 0) { + do_log(sprintf("%s, no user receive email notification", $logMsg)); + return 0; + } + $toolRep = new ToolRepository(); + $categoryName = $torrent->basic_category->name; + $torrentUploader = $torrent->user; + $successCount = 0; + while (true) { + $logPage = "$logMsg, page: $page"; + $users = (clone $query)->with(['language'])->forPage($page, $size)->get(['id', 'email', 'lang']); + if ($users->isEmpty()) { + do_log(sprintf("%s, no more user", $logPage)); + break; + } + foreach ($users as $user) { + $locale = $user->locale; + $logUser = "$logPage, user $user->id, locale: $locale"; + $subject = nexus_trans("upload.email_notification_subject", [ + 'site_name' => Setting::getSiteName() + ], $locale); + $body = nexus_trans("upload.email_notification_body", [ + 'site_name' => Setting::getSiteName(), + 'name' => $torrent->name, + 'size' => mksize($torrent->size), + 'category' => $categoryName, + 'upload_by' => $this->handleAnonymous($torrentUploader->username, $torrentUploader, $user, $torrent), + 'description' => Str::limit(strip_tags(format_comment($torrent->extra->descr)), 500), + 'torrent_url' => sprintf("%s/details.php?id=%s&hit=1", getBaseUrl(), $torrent->id), + ], $locale); + $sendResult = $toolRep->sendMail($user->email, $subject, $body); + do_log(sprintf("%s, send result: %s", $logUser, $sendResult)); + if ($sendResult) { + $successCount++; + } + } + $page++; + } + do_log("$logMsg, receive email notification user total: $total, successCount: $successCount, done!"); + return $successCount; + } + + + } diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 71a11b39..0dad069b 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -4,6 +4,7 @@ namespace App\Repositories; use App\Exceptions\InsufficientPermissionException; use App\Exceptions\NexusException; use App\Http\Resources\ExamUserResource; +use App\Http\Resources\TorrentResource; use App\Http\Resources\UserResource; use App\Models\ExamUser; use App\Models\Invite; @@ -11,12 +12,15 @@ use App\Models\LoginLog; use App\Models\Message; use App\Models\Setting; use App\Models\Snatch; +use App\Models\Torrent; use App\Models\User; use App\Models\UserBanLog; use App\Models\UserMeta; use App\Models\UserModifyLog; use App\Models\UsernameChangeLog; +use App\Utils\ApiQueryBuilder; use Carbon\Carbon; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -53,28 +57,30 @@ class UserRepository extends BaseRepository return $user; } - public function getDetail($id) + public function getDetail($id, Authenticatable $currentUser) { - $with = [ - 'inviter' => function ($query) {return $query->select(User::$commonFields);}, - 'valid_medals' - ]; - $user = User::query()->with($with)->findOrFail($id); - $userResource = new UserResource($user); - $baseInfo = $userResource->response()->getData(true)['data']; + //query this info default + $query = User::query()->with([]); + $allowIncludes = ['inviter', 'valid_medals']; + $allowIncludeCounts = []; + $allowIncludeFields = []; + $apiQueryBuilder = ApiQueryBuilder::for(UserResource::NAME, $query) + ->allowIncludes($allowIncludes) + ->allowIncludeCounts($allowIncludeCounts) + ->allowIncludeFields($allowIncludeFields) + ; + $user = $apiQueryBuilder->build()->findOrFail($id); + return $this->appendIncludeFields($apiQueryBuilder, $currentUser, $user); + } - $examRep = new ExamRepository(); - $examProgress = $examRep->getUserExamProgress($id, ExamUser::STATUS_NORMAL); - if ($examProgress) { - $examResource = new ExamUserResource($examProgress); - $examInfo = $examResource->response()->getData(true)['data']; - } else { - $examInfo = null; - } - return [ - 'base_info' => $baseInfo, - 'exam_info' => $examInfo, - ]; + private function appendIncludeFields(ApiQueryBuilder $apiQueryBuilder, Authenticatable $currentUser, User $user): User + { +// $id = $torrent->id; +// if ($apiQueryBuilder->hasIncludeField('has_bookmarked')) { +// $torrent->has_bookmarked = (int)$user->bookmarks()->where('torrentid', $id)->exists();; +// } + + return $user; } /** diff --git a/app/Utils/ApiQueryBuilder.php b/app/Utils/ApiQueryBuilder.php new file mode 100644 index 00000000..9759296d --- /dev/null +++ b/app/Utils/ApiQueryBuilder.php @@ -0,0 +1,235 @@ +allowedIncludes = $includes; + $requestIncludesStr = $this->request->input(self::PARAM_NAME_INCLUDES); + $this->requestIncludes = explode(',', $requestIncludesStr); + return $this; + } + public function allowFilters(array $filters): self { $this->allowedFilters = $filters; return $this; } + public function allowSorts(array $sorts): self + { + $this->allowedSorts = $sorts; + $requestSortsStr = $this->request->input(self::PARAM_NAME_SORTS, ""); + foreach (explode(',', $requestSortsStr) as $sort) { + $direction = str_starts_with($sort, '-') ? 'desc' : 'asc'; + $field = ltrim($sort, '-'); + $this->requestSorts[$field] = $direction; + } + return $this; + } + public function allowIncludeCounts(array $counts): self { $this->allowedIncludeCounts = $counts; return $this; } + + public function allowIncludeFields(array $fields): self + { + $this->allowedIncludeFields = $fields; + $requestIncludeFieldsStr = $this->request->input(sprintf("%s.%s", self::PARAM_NAME_INCLUDE_FIELDS, $this->resourceName), ""); + $this->requestIncludeFields = explode(',', $requestIncludeFieldsStr); + return $this; + } + + public function build(): Builder + { + $this->applyIncludes(); + $this->applyFilters(); + $this->applyOrFilters(); + $this->applyCustomFilters(); + $this->applySorts(); + $this->applyIncludeCounts(); + return $this->query; + } + + protected function applyIncludes(): void + { + $includes = explode(',', $this->request->query(self::PARAM_NAME_INCLUDES, '')); + $valid = array_intersect($this->allowedIncludes, $includes); + $this->query->with($valid); + } + + protected function applyIncludeCounts(): void + { + $includeCounts = explode(',', $this->request->query(self::PARAM_NAME_INCLUDE_COUNTS, '')); + $valid = array_intersect($this->allowedIncludeCounts, $includeCounts); + do_log(sprintf( + "includeCounts: %s, allow: %s, valid: %s", + json_encode($includeCounts), json_encode($this->allowedIncludeCounts), json_encode($valid) + )); + $this->query->withCount($valid); + } + + protected function applyFilters(): void + { + $filters = $this->request->input(self::PARAM_NAME_FILTER, []); + +// dd($filters); + + foreach ($filters as $field => $value) { + if (!in_array($field, $this->allowedFilters)) continue; + if (isset($this->customFilterCallbacks[$field])) continue; + + // 如果是复杂条件 + if (is_array($value)) { + foreach ($value as $operator => $val) { + $this->applyFilterOperator($field, $operator, $val); + } + } else { + // 简单形式,默认等于 + $this->query->where($field, '=', $value); + } + } + } + + protected function applyFilterOperator(string $field, string $operator, mixed $value): void + { + match ($operator) { + 'eq' => $this->query->where($field, '=', $value), + 'gt' => $this->query->where($field, '>', $value), + 'lt' => $this->query->where($field, '<', $value), + 'gte' => $this->query->where($field, '>=', $value), + 'lte' => $this->query->where($field, '<=', $value), + 'like' => $this->query->where($field, 'like', $value), + 'in' => $this->query->whereIn($field, is_array($value) ? $value : explode(',', $value)), + default => null + }; + } + + protected function applyOrFilters(): void + { + $filters = $this->request->input(self::PARAM_NAME_FILTER_ANY, []); + + if (!empty($filters)) { + $this->query->where(function ($q) use ($filters) { + foreach ($filters as $field => $value) { + if (!in_array($field, $this->allowedFilters)) continue; + if (isset($this->customFilterCallbacks[$field])) continue; + + if (is_array($value)) { + foreach ($value as $operator => $val) { + $this->applyFilterAnyOperator($q, $field, $operator, $val); + } + } else { + $q->orWhere($field, '=', $value); + } + } + }); + } + } + + protected function applyFilterAnyOperator(Builder $query, string $field, string $operator, mixed $value): void + { + match ($operator) { + 'eq' => $query->orWhere($field, '=', $value), + 'gt' => $query->orWhere($field, '>', $value), + 'lt' => $query->orWhere($field, '<', $value), + 'gte' => $query->orWhere($field, '>=', $value), + 'lte' => $query->orWhere($field, '<=', $value), + 'like' => $query->orWhere($field, 'like', $value), + 'in' => $query->orWhereIn($field, is_array($value) ? $value : explode(',', $value)), + default => null + }; + } + + public function registerCustomFilter(string $field, callable $callback): self + { + $this->customFilterCallbacks[$field] = $callback; + return $this; + } + + protected function applyCustomFilters(): void + { + foreach ($this->customFilterCallbacks as $field => $callback) { + call_user_func($callback, $this->query, $this->request); + } + } + + + + protected function applySorts(): void + { + $sorts = explode(',', $this->request->query(self::PARAM_NAME_SORTS, '')); + foreach ($sorts as $sort) { + $direction = str_starts_with($sort, '-') ? 'desc' : 'asc'; + $field = ltrim($sort, '-'); + if (in_array($field, $this->allowedSorts)) { + $this->query->orderBy($field, $direction); + } + } + } + + public function hasIncludeField(string $field = null): bool + { + return $this->hasBoth($this->allowedIncludeFields, $this->requestIncludeFields, $field); + } + + public function hasInclude(string $name = null): bool + { + return $this->hasBoth($this->allowedIncludes, $this->requestIncludes, $name); + } + + public function hasSort(string $field = ""):bool + { + return $this->hasBoth($this->allowedSorts, array_keys($this->requestSorts), $field); + } + + private function hasBoth(array $dataA, array $dataB, mixed $oneKey = null): bool + { + $result = array_intersect($dataA, $dataB); + if (empty($oneKey)) { + return !empty($result); + } + return in_array($oneKey, $result); + } + + +} diff --git a/composer.json b/composer.json index 80f5ca5a..825356eb 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,11 @@ "ext-mbstring": "*", "ext-mysqli": "*", "ext-pcntl": "*", + "ext-posix": "*", "ext-redis": "*", "ext-xml": "*", "ext-zend-opcache": "*", "ext-zip": "*", - "ext-posix": "*", "calebporzio/sushi": "^2.5", "elasticsearch/elasticsearch": "^7.16", "filament/filament": "^3.2", @@ -51,7 +51,8 @@ "orangehill/iseed": "^3.0", "phpgangsta/googleauthenticator": "dev-master", "rhilip/bencode": "^2.0", - "rlanvin/php-ip": "^3.0" + "rlanvin/php-ip": "^3.0", + "stichoza/google-translate-php": "^5.2" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index 4487e0f6..efc3f722 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4391f5fb2633f1fa33c4099a1b176b78", + "content-hash": "e1de7765d56f7550f8cae764df3f7753", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -6965,6 +6965,87 @@ ], "time": "2024-12-30T13:13:39+00:00" }, + { + "name": "stichoza/google-translate-php", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/Stichoza/google-translate-php.git", + "reference": "9429773d991c98f68a25bec40d20f590ea3312a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Stichoza/google-translate-php/zipball/9429773d991c98f68a25bec40d20f590ea3312a0", + "reference": "9429773d991c98f68a25bec40d20f590ea3312a0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.0", + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Stichoza\\GoogleTranslate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Levan Velijanashvili", + "email": "me@stichoza.com" + } + ], + "description": "Free Google Translate API PHP Package", + "homepage": "https://github.com/Stichoza/google-translate-php", + "keywords": [ + "google", + "php", + "translate", + "translating", + "translator" + ], + "support": { + "issues": "https://github.com/Stichoza/google-translate-php/issues", + "source": "https://github.com/Stichoza/google-translate-php/tree/v5.2.0" + }, + "funding": [ + { + "url": "https://btc.com/bc1qc25j4x7yahghm8nnn6lypnw59nptylsw32nkfl", + "type": "custom" + }, + { + "url": "https://www.paypal.me/stichoza", + "type": "custom" + }, + { + "url": "https://ko-fi.com/stichoza", + "type": "ko_fi" + }, + { + "url": "https://liberapay.com/stichoza", + "type": "liberapay" + }, + { + "url": "https://opencollective.com/stichoza", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/stichoza", + "type": "patreon" + } + ], + "time": "2024-08-05T19:11:36+00:00" + }, { "name": "symfony/clock", "version": "v7.2.0", @@ -13044,11 +13125,12 @@ "ext-mbstring": "*", "ext-mysqli": "*", "ext-pcntl": "*", + "ext-posix": "*", "ext-redis": "*", "ext-xml": "*", "ext-zend-opcache": "*", "ext-zip": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/config/logging.php b/config/logging.php index afefa71f..3c765152 100644 --- a/config/logging.php +++ b/config/logging.php @@ -44,14 +44,15 @@ return [ 'single' => [ 'driver' => 'single', 'tap' => [\App\Logging\NexusFormatter::class], - 'path' => env('LOG_FILE', '/tmp/nexus.log'), + 'path' => getLogFile(), 'level' => env('LOG_LEVEL', 'debug'), 'ignore_exceptions' => false, ], 'daily' => [ 'driver' => 'daily', - 'path' => env('LOG_FILE', '/tmp/nexus.log'), +// 'path' => env('LOG_FILE', '/tmp/nexus.log'), + 'path' => getLogFile(), 'level' => env('LOG_LEVEL', 'debug'), 'tap' => [\App\Logging\NexusFormatter::class], 'days' => 14, @@ -104,7 +105,8 @@ return [ ], 'emergency' => [ - 'path' => env('LOG_FILE', '/tmp/nexus.log'), +// 'path' => env('LOG_FILE', '/tmp/nexus.log'), + 'path' => getLogFile(), ], ], diff --git a/database/migrations/2025_02_09_231747_create_personal_access_token_plain_table.php b/database/migrations/2025_04_10_145925_add_uid_to_sitelog_table.php similarity index 53% rename from database/migrations/2025_02_09_231747_create_personal_access_token_plain_table.php rename to database/migrations/2025_04_10_145925_add_uid_to_sitelog_table.php index 75fb32f2..04436305 100644 --- a/database/migrations/2025_02_09_231747_create_personal_access_token_plain_table.php +++ b/database/migrations/2025_04_10_145925_add_uid_to_sitelog_table.php @@ -11,11 +11,8 @@ return new class extends Migration */ public function up(): void { - Schema::create('personal_access_token_plains', function (Blueprint $table) { - $table->id(); - $table->bigInteger('access_token_id')->unsigned(); - $table->string("plain_text_token"); - $table->timestamps(); + Schema::table('sitelog', function (Blueprint $table) { + $table->integer('uid')->default(0); }); } @@ -24,6 +21,8 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('personal_access_token_plains'); + Schema::table('sitelog', function (Blueprint $table) { + $table->dropColumn('uid'); + }); } }; diff --git a/include/constants.php b/include/constants.php index e0fde4b5..7217a03e 100644 --- a/include/constants.php +++ b/include/constants.php @@ -1,6 +1,6 @@ insert([ + 'added' => now(), + 'txt' => $text, + 'security_level' => $security, + 'uid' => get_user_id(), + ]); } @@ -2219,9 +2222,8 @@ function validlang($langid) { function get_if_restricted_is_open() { - global $sptime; // it's sunday - if($sptime == 'yes' && (date("w",time()) == '0' || (date("w",time()) == 6) && (date("G",time()) >=12 && date("G",time()) <=23))) + if(\App\Models\Setting::getIsUploadOpenAtWeekend() && (date("w",time()) == '0' || (date("w",time()) == 6) && (date("G",time()) >=12 && date("G",time()) <=23))) { return true; } @@ -2992,7 +2994,6 @@ function set_langfolder_cookie($folder, $expires = 0x7fffffff) function get_protocol_prefix() { - global $securelogin; if (isHttps()) { return "https://"; } @@ -4277,7 +4278,18 @@ function permissiondenied($allowMinimumClass = null){ } function gettime($time, $withago = true, $twoline = false, $forceago = false, $oneunit = false, $isfuturetime = false){ - global $lang_functions, $CURUSER; + if (!IN_NEXUS) { + if (empty($time)) { + return null; + } + try { + return \Carbon\Carbon::parse($time)->diffForHumans(); + } catch (\Exception $e) { + do_log($e->getMessage() . $e->getTraceAsString(), 'error'); + return $time; + } + } + global $lang_functions, $CURUSER; if (isset($CURUSER) && $CURUSER['timetype'] != 'timealive' && !$forceago){ $newtime = $time; if ($twoline){ @@ -5910,7 +5922,8 @@ function insert_torrent_tags($torrentId, $tagIdArr, $sync = false) $canSetSpecialTag = user_can('torrent-set-special-tag'); $dateTimeStringNow = date('Y-m-d H:i:s'); if ($sync) { - sql_query("delete from torrent_tags where torrent_id = $torrentId"); + \App\Models\TorrentTag::query()->where("torrent_id", $torrentId)->delete(); +// sql_query("delete from torrent_tags where torrent_id = $torrentId"); } if (empty($tagIdArr)) { return; @@ -5926,7 +5939,8 @@ function insert_torrent_tags($torrentId, $tagIdArr, $sync = false) } $insertTagsSql .= implode(', ', $values); do_log("[INSERT_TAGS], torrent: $torrentId with tags: " . nexus_json_encode($tagIdArr)); - sql_query($insertTagsSql); + \Nexus\Database\NexusDB::statement($insertTagsSql); +// sql_query($insertTagsSql); } function get_smile($num) diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 34b24376..d6752daa 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -242,6 +242,9 @@ function getLogFile($append = '') return $logFiles[$append]; } $config = nexus_config('nexus'); + if (!empty($config['log_file']) && in_array($config['log_files'], ["/dev/stdout", "/dev/stderr"])) { + return $logFiles[$append] = $config['log_files']; + } $path = getenv('NEXUS_LOG_DIR', true); $fromEnv = true; if ($path === false) { @@ -500,12 +503,14 @@ function getSchemeAndHttpHost(bool $fromConfig = false) function getBaseUrl() { $url = getSchemeAndHttpHost(); - $requestUri = $_SERVER['REQUEST_URI']; - $pos = strpos($requestUri, '?'); - if ($pos !== false) { - $url .= substr($requestUri, 0, $pos); - } else { - $url .= $requestUri; + if (!isRunningInConsole()) { + $requestUri = $_SERVER['REQUEST_URI']; + $pos = strpos($requestUri, '?'); + if ($pos !== false) { + $url .= substr($requestUri, 0, $pos); + } else { + $url .= $requestUri; + } } return trim($url, '/'); } @@ -528,13 +533,10 @@ function api(...$args) $msg = $args[1]; $data = $args[2]; } - if ($data instanceof \Illuminate\Http\Resources\Json\ResourceCollection || $data instanceof \Illuminate\Http\Resources\Json\JsonResource) { + if ($data instanceof \Illuminate\Http\Resources\Json\JsonResource) { $data = $data->response()->getData(true); - if (isset($data['data']) && count($data) == 1) { - //单纯的集合,无分页等其数据 - $data = $data['data']; - } } +// dd($data); $time = (float)number_format(microtime(true) - nexus()->getStartTimestamp(), 3); $count = null; $resultKey = 'ret'; @@ -630,7 +632,7 @@ function last_query($all = false) function format_datetime($datetime, $format = 'Y-m-d H:i') { if (empty($datetime)) { - return ''; + return null; } try { $carbonTime = \Carbon\Carbon::parse($datetime); @@ -1320,15 +1322,28 @@ function get_snatch_info($torrentId, $userId) */ function fire_event(string $name, \Illuminate\Database\Eloquent\Model $model, \Illuminate\Database\Eloquent\Model $oldModel = null): void { - $prefix = "fire_event:"; - $idKey = $prefix . \Illuminate\Support\Str::random(); - $idKeyOld = ""; - \Nexus\Database\NexusDB::cache_put($idKey, serialize($model), 3600*24*30); - if ($oldModel) { - $idKeyOld = $prefix . \Illuminate\Support\Str::random(); - \Nexus\Database\NexusDB::cache_put($idKeyOld, serialize($oldModel), 3600*24*30); + if (!isset(\App\Enums\ModelEventEnum::$eventMaps[$name])) { + throw new \InvalidArgumentException("Event $name is not a valid event enumeration"); + } + if (IN_NEXUS) { + $prefix = "fire_event:"; + $idKey = $prefix . \Illuminate\Support\Str::random(); + $idKeyOld = ""; + \Nexus\Database\NexusDB::cache_put($idKey, serialize($model), 3600*24*30); + if ($oldModel) { + $idKeyOld = $prefix . \Illuminate\Support\Str::random(); + \Nexus\Database\NexusDB::cache_put($idKeyOld, serialize($oldModel), 3600*24*30); + } + executeCommand("event:fire --name=$name --idKey=$idKey --idKeyOld=$idKeyOld", "string", true, false); + } else { + $eventClass = \App\Enums\ModelEventEnum::$eventMaps[$name]['event']; + $params = [$model]; + if ($oldModel) { + $params[] = $oldModel; + } + call_user_func_array([$eventClass, "dispatch"], $params); + publish_model_event($name, $model->id); } - executeCommand("event:fire --name=$name --idKey=$idKey --idKeyOld=$idKeyOld", "string", true, false); } /** diff --git a/lang/chs/lang_log.php b/lang/chs/lang_log.php index 7963efa6..fd793e2a 100644 --- a/lang/chs/lang_log.php +++ b/lang/chs/lang_log.php @@ -12,6 +12,7 @@ $lang_log = array 'title_time_added' => "时间", 'col_date' => "日期", 'col_event' => "事件", + 'col_user' => '用户', 'time_zone_note' => "

时间为北京时间。

\n", 'text_daily_log' => " 常 规 日 志 ", 'text_chronicle' => " 史  册 ", diff --git a/lang/cht/lang_log.php b/lang/cht/lang_log.php index 8916fad5..1bb80dc9 100644 --- a/lang/cht/lang_log.php +++ b/lang/cht/lang_log.php @@ -12,6 +12,7 @@ $lang_log = array 'title_time_added' => "時間", 'col_date' => "日期", 'col_event' => "事件", + 'col_user' => '用戶', 'time_zone_note' => "

時間為北京時間。

\n", 'text_daily_log' => " 常 規 日 志 ", 'text_chronicle' => " 史  冊 ", diff --git a/lang/en/lang_log.php b/lang/en/lang_log.php index 87875962..9617d74e 100644 --- a/lang/en/lang_log.php +++ b/lang/en/lang_log.php @@ -43,6 +43,7 @@ $lang_log = array 'text_news_empty' => "News is empty
", 'col_title' => "Title", 'col_body' => "Body", + 'col_user' => 'User', 'std_delete_poll' => "Delete poll", 'std_delete_poll_confirmation' => "Do you really want to delete a poll? Click ", 'std_here_if_sure' => "here if you are sure.", diff --git a/nexus/Torrent/TechnicalInformation.php b/nexus/Torrent/TechnicalInformation.php index 048f4d8f..a3eb6251 100644 --- a/nexus/Torrent/TechnicalInformation.php +++ b/nexus/Torrent/TechnicalInformation.php @@ -148,20 +148,25 @@ class TechnicalInformation public function renderOnDetailsPage() { global $lang_functions; - $videos = [ - 'Runtime' => $this->getRuntime(), - 'Resolution' => $this->getResolution(), - 'Bitrate' => $this->getBitrate(), - 'HDR' => $this->getHDRFormat(), - 'Bit depth' => $this->getBitDepth(), - 'Frame rate' => $this->getFramerate(), - 'Profile' => $this->getProfile(), - 'Ref.Frames' => $this->getRefFrame(), - ]; - $videos = array_filter($videos); - $audios = $this->getAudios(); - $subtitles = $this->getSubtitles(); -// dd($videos, $audios, $subtitles); +// $videos = [ +// 'Runtime' => $this->getRuntime(), +// 'Resolution' => $this->getResolution(), +// 'Bitrate' => $this->getBitrate(), +// 'HDR' => $this->getHDRFormat(), +// 'Bit depth' => $this->getBitDepth(), +// 'Frame rate' => $this->getFramerate(), +// 'Profile' => $this->getProfile(), +// 'Ref.Frames' => $this->getRefFrame(), +// ]; +// $videos = array_filter($videos); +// $audios = $this->getAudios(); +// $subtitles = $this->getSubtitles(); + $summaryInfo = $this->getSummaryInfo(); + $videos = $summaryInfo['videos'] ?: []; + $audios = $summaryInfo['audios'] ?: []; + $subtitles = $summaryInfo['subtitles'] ?: []; + +// dd($summaryInfo, $videos, $audios, $subtitles); if (empty($videos) && empty($audios) && empty($subtitles)) { return sprintf('
%s
', $this->mediaInfo); } @@ -187,6 +192,24 @@ class TechnicalInformation return $result; } + public function getSummaryInfo(): array + { + $videos = [ + 'Runtime' => $this->getRuntime(), + 'Resolution' => $this->getResolution(), + 'Bitrate' => $this->getBitrate(), + 'HDR' => $this->getHDRFormat(), + 'Bit depth' => $this->getBitDepth(), + 'Frame rate' => $this->getFramerate(), + 'Profile' => $this->getProfile(), + 'Ref.Frames' => $this->getRefFrame(), + ]; + $videos = array_filter($videos) ?: null; + $audios = $this->getAudios() ?: null; + $subtitles = $this->getSubtitles() ?: null; + return compact('videos', 'audios', 'subtitles'); + } + private function buildTdTable(array $parts) { $table = ''; diff --git a/public/log.php b/public/log.php index 49941747..5c24788f 100644 --- a/public/log.php +++ b/public/log.php @@ -121,7 +121,7 @@ else { list($pagertop, $pagerbottom, $limit) = pager($perpage, $count, "log.php?action=dailylog&".$addparam); - $res = sql_query("SELECT added, txt FROM sitelog $wherea ORDER BY added DESC $limit") or sqlerr(__FILE__, __LINE__); + $res = sql_query("SELECT * FROM sitelog $wherea ORDER BY added DESC $limit") or sqlerr(__FILE__, __LINE__); if (mysql_num_rows($res) == 0) print($lang_log['text_log_empty']); else @@ -130,7 +130,11 @@ else { //echo $pagertop; print("
\n"); - print("\n"); + print(""); + } + print("\n"); while ($arr = mysql_fetch_assoc($res)) { $color = ""; @@ -139,7 +143,11 @@ else { if (strpos($arr['txt'],'was added to the Request section')) $color = "purple"; if (strpos($arr['txt'],'was edited by')) $color = "blue"; if (strpos($arr['txt'],'settings updated by')) $color = "darkred"; - print("\n"); + print(""); + if (user_can('confilog')){ + print(""); + } + print("\n"); } print("
\"time\"".$lang_log['col_event']."
\"time\"".$lang_log['col_event']); + if (user_can('confilog')){ + print("".$lang_log['col_user']."
".gettime($arr['added'],true,false)."".htmlspecialchars($arr['txt'])."
".gettime($arr['added'],true,false)."".htmlspecialchars($arr['txt'])."".($arr['uid'] > 0 ? get_username($arr['uid']) : "System")."
"); diff --git a/public/recover.php b/public/recover.php index d5ba6976..1cfaaa85 100644 --- a/public/recover.php +++ b/public/recover.php @@ -34,7 +34,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") failedlogins($lang_recover['std_missing_email_address'],true); if (!check_email($email)) failedlogins($lang_recover['std_invalid_email_address'],true); - $res = sql_query("SELECT * FROM users WHERE email=" . sqlesc($email) . " LIMIT 1") or sqlerr(__FILE__, __LINE__); + $res = sql_query("SELECT * FROM users WHERE BINARY email=" . sqlesc($email) . " LIMIT 1") or sqlerr(__FILE__, __LINE__); $arr = mysql_fetch_assoc($res); if (!$arr) failedlogins($lang_recover['std_email_not_in_database'],true); if ($arr['status'] == "pending") failedlogins($lang_recover['std_user_account_unconfirmed'],true); diff --git a/public/settings.php b/public/settings.php index 25308d9f..e5fc681c 100644 --- a/public/settings.php +++ b/public/settings.php @@ -746,7 +746,7 @@ elseif ($action == 'torrentsettings') tr($lang_settings['row_tax_factor']," ".$lang_settings['text_tax_factor_note'], 1); tr($lang_settings['row_max_price']," ".$lang_settings['text_max_price_note'], 1); - yesorno($lang_settings['row_promotion_rules'], 'prorules', $TORRENT["prorules"], $lang_settings['text_promotion_rules_note']); +// yesorno($lang_settings['row_promotion_rules'], 'prorules', $TORRENT["prorules"], $lang_settings['text_promotion_rules_note']); tr($lang_settings['row_random_promotion'], $lang_settings['text_random_promotion_note_one']."".$lang_settings['text_random_promotion_note_two'], 1); tr($lang_settings['row_large_torrent_promotion'], $lang_settings['text_torrent_larger_than']."".$lang_settings['text_gb_promoted_to']."".$lang_settings['text_by_system_upon_uploading']."
".$lang_settings['text_large_torrent_promotion_note'], 1); tr($lang_settings['row_promotion_timeout'], $lang_settings['text_promotion_timeout_note_one']."