improve admin user profile + image hosting

This commit is contained in:
xiaomlove
2024-12-29 22:16:41 +08:00
parent f146a654c2
commit 5a9f1f1017
40 changed files with 22110 additions and 456 deletions
+4
View File
@@ -92,4 +92,8 @@ class NexusWebUserProvider implements UserProvider
return true;
}
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// TODO: Implement rehashPasswordIfRequired() method.
}
}
+3 -1
View File
@@ -102,7 +102,9 @@ class Test extends Command
public function handle()
{
$str = "1.abc.de";
$res = explode(".", $str, 2);
$ext = "png";
$str = "202404/20240403215909f58f38ddd968a0e8a4bdd30690a9e92e.png";
$res = substr($str, 0,-1*strlen($ext)-1);
dd($res);
}
@@ -7,27 +7,21 @@ use App\Filament\Resources\System\SettingResource;
use App\Models\HitAndRun;
use App\Models\SearchBox;
use App\Models\Setting;
use App\Models\Tag;
use App\Models\User;
use App\Repositories\MeiliSearchRepository;
use Filament\Facades\Filament;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
use Filament\Resources\Pages\EditRecord;
use Filament\Resources\Pages\Page;
use Filament\Forms;
use Illuminate\Support\Facades\DB;
use Nexus\Database\NexusDB;
class EditSetting extends Page implements Forms\Contracts\HasForms
{
use InteractsWithForms, OptionsTrait;
use Forms\Concerns\InteractsWithForms, OptionsTrait;
protected static string $resource = SettingResource::class;
protected static string $view = 'filament.resources.system.setting-resource.pages.edit-hit-and-run';
public ?array $data = [];
public function getTitle(): string
{
return __('label.setting.nav_text');
@@ -39,12 +33,21 @@ class EditSetting extends Page implements Forms\Contracts\HasForms
$this->fillForm();
}
public function form(Forms\Form $form): Forms\Form
{
return $form
->schema($this->getFormSchema())
->statePath('data');
}
private function fillForm()
{
$settings = Setting::get();
$settings = Setting::getFromDb();
$this->form->fill($settings);
}
protected function getFormSchema(): array
{
return [
@@ -81,7 +84,7 @@ class EditSetting extends Page implements Forms\Contracts\HasForms
Setting::query()->upsert($data, ['name'], ['value']);
do_action("nexus_setting_update");
clear_setting_cache();
Filament::notify('success', __('filament::resources/pages/edit-record.messages.saved'), true);
send_admin_success_notification();
}
private function getTabs(): array
@@ -125,6 +128,13 @@ class EditSetting extends Page implements Forms\Contracts\HasForms
->columns(2)
;
$id = "image_hosting";
$tabs[] = Forms\Components\Tabs\Tab::make(__("label.setting.$id.tab_header"))
->id($id)
->schema($this->getTabImageHostingSchema($id))
->columns(2)
;
$tabs[] = Forms\Components\Tabs\Tab::make(__('label.setting.system.tab_header'))
->id('system')
->schema([
@@ -184,7 +194,7 @@ class EditSetting extends Page implements Forms\Contracts\HasForms
return apply_filter("hit_and_run_setting_schema", $default);
}
private function getTabMeilisearchSchema($id)
private function getTabMeilisearchSchema($id): array
{
$schema = [];
@@ -215,4 +225,59 @@ class EditSetting extends Page implements Forms\Contracts\HasForms
return $schema;
}
private function getTabImageHostingSchema($id): array
{
$schema = [];
$name = "$id.driver";
$schema[] = Forms\Components\Radio::make($name)
->options(['local' => 'local', 'chevereto' => 'chevereto', 'lsky' => 'lsky'])
->inline(true)
->label(__("label.setting.$name"))
->helperText(__("label.setting.{$name}_help"))
->columnSpanFull()
;
//chevereto
$driverName = "chevereto";
$driverId = sprintf("%s_%s", $id, $driverName);
$driverSchemas = [];
$field = "upload_api_endpoint";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$field = "upload_token";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$field = "base_url";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$driverSection = Forms\Components\Section::make($driverName)->schema($driverSchemas);
$schema[] = $driverSection;
//lsky
$driverName = "lsky";
$driverId = sprintf("%s_%s", $id, $driverName);
$driverSchemas = [];
$field = "upload_api_endpoint";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$field = "upload_token";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$field = "base_url";
$driverSchemas[] = Forms\Components\TextInput::make("$driverId.$field")
->label(__("label.setting.$id.$field"))
;
$driverSection = Forms\Components\Section::make($driverName)->schema($driverSchemas);
$schema[] = $driverSection;
return $schema;
}
}
@@ -100,7 +100,7 @@ class ExamUserResource extends Resource
->actions([
Tables\Actions\ViewAction::make(),
])
->prependBulkActions([
->groupedBulkActions([
Tables\Actions\BulkAction::make('Avoid')->action(function (Collection $records) {
$idArr = $records->pluck('id')->toArray();
$rep = new ExamRepository();
@@ -75,7 +75,7 @@ class HitAndRunResource extends Resource
->actions([
Tables\Actions\ViewAction::make(),
])
->prependBulkActions([
->groupedBulkActions([
Tables\Actions\BulkAction::make('Pardon')->action(function (Collection $records) {
$idArr = $records->pluck('id')->toArray();
$rep = new HitAndRunRepository();
+208 -2
View File
@@ -7,12 +7,16 @@ use App\Filament\Resources\User\UserResource\Pages;
use App\Filament\Resources\User\UserResource\RelationManagers;
use App\Models\User;
use App\Repositories\UserRepository;
use Filament\Actions\Action;
use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Table;
use Filament\Tables;
use Filament\Infolists;
use Filament\Infolists\Infolist;
use Filament\Infolists\Components;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
@@ -32,6 +36,16 @@ class UserResource extends Resource
protected static ?int $navigationSort = 1;
private static $rep;
private static function getRep(): UserRepository
{
if (!self::$rep) {
self::$rep = new UserRepository();
}
return self::$rep;
}
public static function getNavigationLabel(): string
{
return __('admin.sidebar.users_list');
@@ -102,6 +116,198 @@ class UserResource extends Resource
->bulkActions(self::getBulkActions());
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Components\Grid::make(2)->schema([
Components\Group::make([
Infolists\Components\TextEntry::make('id')->label("UID"),
Infolists\Components\TextEntry::make('username')
->label(__("label.user.username"))
->formatStateUsing(fn ($record) => get_username($record->id, false, true, true, true))
->html()
,
Infolists\Components\TextEntry::make('email')
->label(__("label.email"))
,
Infolists\Components\TextEntry::make('passkey'),
Infolists\Components\TextEntry::make('added')->label(__("label.added")),
Infolists\Components\TextEntry::make('last_access')->label(__("label.last_access")),
Infolists\Components\TextEntry::make('inviter.username')->label(__("label.user.invite_by")),
Infolists\Components\TextEntry::make('parked')->label(__("label.user.parked")),
Infolists\Components\TextEntry::make('offer_allowed_count')->label(__("label.user.offer_allowed_count")),
Infolists\Components\TextEntry::make('seed_points')->label(__("label.user.seed_points")),
Infolists\Components\TextEntry::make('uploadedText')->label(__("label.uploaded")),
Infolists\Components\TextEntry::make('downloadedText')->label(__("label.downloaded")),
Infolists\Components\TextEntry::make('seedbonus')->label(__("label.user.seedbonus")),
])->columns(2),
Components\Group::make([
Infolists\Components\TextEntry::make('status')
->label(__('label.user.status'))
->badge()
->colors(['success' => User::STATUS_CONFIRMED, 'warning' => User::STATUS_PENDING])
->hintAction(self::buildActionConfirm())
,
Infolists\Components\TextEntry::make('classText')
->label(__("label.user.class"))
->hintAction(self::buildActionChangeClass())
,
Infolists\Components\TextEntry::make('enabled')
->label(__("label.user.enabled"))
->badge()
->colors(['success' => 'yes', 'warning' => 'no'])
->hintAction(self::buildActionEnableDisable())
,
Infolists\Components\TextEntry::make('downloadpos')
->label(__("label.user.downloadpos"))
->badge()
->colors(['success' => 'yes', 'warning' => 'no'])
->hintAction(self::buildActionChangeDownloadPos())
,
Infolists\Components\TextEntry::make('twoFactorAuthenticationStatus')
->label(__("label.user.two_step_authentication"))
->badge()
->colors(['success' => 'yes', 'warning' => 'no'])
->hintAction(self::buildActionCancelTwoStepAuthentication())
,
])
]),
]);
}
private static function buildActionChangeClass(): Infolists\Components\Actions\Action
{
return Infolists\Components\Actions\Action::make("changeClass")
->label(__('label.change'))
->button()
->visible(fn (User $record): bool => (Auth::user()->class > $record->class))
->form([
Forms\Components\Select::make('class')
->options(User::listClass(User::CLASS_PEASANT, Auth::user()->class - 1))
->default(fn (User $record) => $record->class)
->label(__('user.labels.class'))
->required()
->reactive()
,
Forms\Components\Radio::make('vip_added')
->options(self::getYesNoOptions('yes', 'no'))
->default(fn (User $record) => $record->vip_added)
->label(__('user.labels.vip_added'))
->helperText(__('user.labels.vip_added_help'))
->hidden(fn (\Filament\Forms\Get $get) => $get('class') != User::CLASS_VIP)
,
Forms\Components\DateTimePicker::make('vip_until')
->default(fn (User $record) => $record->vip_until)
->label(__('user.labels.vip_until'))
->helperText(__('user.labels.vip_until_help'))
->hidden(fn (\Filament\Forms\Get $get) => $get('class') != User::CLASS_VIP)
,
Forms\Components\TextInput::make('reason')
->label(__('admin.resources.user.actions.enable_disable_reason'))
->placeholder(__('admin.resources.user.actions.enable_disable_reason_placeholder'))
,
])
->action(function (User $record, array $data) {
$userRep = self::getRep();
try {
$userRep->changeClass(Auth::user(), $record, $data['class'], $data['reason'], $data);
send_admin_success_notification();
} catch (\Exception $exception) {
send_admin_fail_notification($exception->getMessage());
}
});
}
private static function buildActionConfirm(): Infolists\Components\Actions\Action
{
return Infolists\Components\Actions\Action::make(__('admin.resources.user.actions.confirm_btn'))
->modalHeading(__('admin.resources.user.actions.confirm_btn'))
->requiresConfirmation()
->visible(fn (User $record): bool => (Auth::user()->class > $record->class))
->button()
->color('success')
->visible(fn ($record) => $record->status == User::STATUS_PENDING)
->action(function (User $record) {
if (Auth::user()->class <= $record->class) {
send_admin_fail_notification("No Permission!");
return;
}
$record->status = User::STATUS_CONFIRMED;
$record->info= null;
$record->save();
send_admin_success_notification();
});
}
private static function buildActionEnableDisable(): Infolists\Components\Actions\Action
{
return Infolists\Components\Actions\Action::make("changeClass")
->label(fn (User $record) => $record->enabled == 'yes' ? __('admin.resources.user.actions.disable_modal_btn') : __('admin.resources.user.actions.enable_modal_btn'))
->modalHeading(fn (User $record) => $record->enabled == 'yes' ? __('admin.resources.user.actions.disable_modal_title') : __('admin.resources.user.actions.enable_modal_title'))
->button()
->visible(fn (User $record): bool => (Auth::user()->class > $record->class))
->form([
Forms\Components\TextInput::make('reason')->label(__('admin.resources.user.actions.enable_disable_reason'))->placeholder(__('admin.resources.user.actions.enable_disable_reason_placeholder')),
Forms\Components\Hidden::make('action')->default(fn (User $record) => $record->enabled == 'yes' ? 'disable' : 'enable'),
Forms\Components\Hidden::make('uid')->default(fn (User $record) => $record->id),
])
->action(function (User $record, array $data) {
$userRep = self::getRep();
try {
if ($data['action'] == 'enable') {
$userRep->enableUser(Auth::user(), $data['uid'], $data['reason']);
} elseif ($data['action'] == 'disable') {
$userRep->disableUser(Auth::user(), $data['uid'], $data['reason']);
}
send_admin_success_notification();
} catch (\Exception $exception) {
send_admin_fail_notification($exception->getMessage());
}
});
}
private static function buildActionChangeDownloadPos(): Infolists\Components\Actions\Action
{
return Infolists\Components\Actions\Action::make("changeDownloadPos")
->label(fn (User $record) => $record->downloadpos == 'yes' ? __('admin.resources.user.actions.disable_download_privileges_btn') : __('admin.resources.user.actions.enable_download_privileges_btn'))
->button()
->requiresConfirmation()
->visible(fn (User $record): bool => (Auth::user()->class > $record->class))
->action(function (User $record) {
$userRep = self::getRep();
try {
$userRep->updateDownloadPrivileges(Auth::user(), $record->id, $record->downloadpos == 'yes' ? 'no' : 'yes');
send_admin_success_notification();
} catch (\Exception $exception) {
send_admin_fail_notification($exception->getMessage());
}
});
}
private static function buildActionCancelTwoStepAuthentication(): Infolists\Components\Actions\Action
{
return Infolists\Components\Actions\Action::make("twoStepAuthentication")
->label(__('admin.resources.user.actions.disable_two_step_authentication'))
->button()
->visible(fn (User $record) => $record->two_step_secret != "")
->modalHeading(__('admin.resources.user.actions.disable_two_step_authentication'))
->requiresConfirmation()
->action(function (user $record) {
$userRep = self::getRep();
try {
$userRep->removeTwoStepAuthentication(Auth::user(), $record->id);
send_admin_success_notification();
} catch (\Exception $exception) {
send_admin_fail_notification($exception->getMessage());
}
});
}
public static function getRelations(): array
{
return [
@@ -123,13 +329,13 @@ class UserResource extends Resource
public static function getBulkActions(): array
{
$actions = [];
if (Auth::user()->class >= User::CLASS_SYSOP) {
if (filament()->auth()->user()->class >= User::CLASS_SYSOP) {
$actions[] = Tables\Actions\BulkAction::make('confirm')
->label(__('admin.resources.user.actions.confirm_bulk'))
->requiresConfirmation()
->deselectRecordsAfterCompletion()
->action(function (Collection $records) {
$rep = new UserRepository();
$rep = self::getRep();
$rep->confirmUser($records->pluck('id')->toArray());
});
}
@@ -13,10 +13,11 @@ use App\Repositories\ExamRepository;
use App\Repositories\MedalRepository;
use App\Repositories\UserRepository;
use Carbon\Carbon;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\Concerns\HasRelationManagers;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
use Filament\Resources\Pages\Page;
use Filament\Pages\Actions;
use Filament\Actions;
use Filament\Forms;
use Filament\Resources\Pages\ViewRecord;
use Illuminate\Support\Facades\Auth;
@@ -32,13 +33,7 @@ class UserProfile extends ViewRecord
protected static string $resource = UserResource::class;
protected static string $view = 'filament.resources.user.user-resource.pages.user-profile';
const EVENT_RECORD_UPDATED = 'recordUpdated';
protected $listeners = [
self::EVENT_RECORD_UPDATED => 'updateRecord'
];
// protected static string $view = 'filament.resources.user.user-resource.pages.user-profile';
private function getRep(): UserRepository
{
@@ -48,11 +43,6 @@ class UserProfile extends ViewRecord
return self::$rep;
}
public function updateRecord($id)
{
$this->record = $this->resolveRecord($id);
}
protected function getHeaderActions(): array
{
$actions = [];
@@ -61,18 +51,18 @@ class UserProfile extends ViewRecord
$actions[] = $this->buildGrantMedalAction();
$actions[] = $this->buildAssignExamAction();
$actions[] = $this->buildChangeBonusEtcAction();
if ($this->record->two_step_secret) {
$actions[] = $this->buildDisableTwoStepAuthenticationAction();
}
if ($this->record->status == User::STATUS_PENDING) {
$actions[] = $this->buildConfirmAction();
}
// if ($this->record->two_step_secret) {
// $actions[] = $this->buildDisableTwoStepAuthenticationAction();
// }
// if ($this->record->status == User::STATUS_PENDING) {
// $actions[] = $this->buildConfirmAction();
// }
$actions[] = $this->buildResetPasswordAction();
$actions[] = $this->buildEnableDisableAction();
$actions[] = $this->buildEnableDisableDownloadPrivilegesAction();
if (user_can('user-change-class')) {
$actions[] = $this->buildChangeClassAction();
}
// $actions[] = $this->buildEnableDisableAction();
// $actions[] = $this->buildEnableDisableDownloadPrivilegesAction();
// if (user_can('user-change-class')) {
// $actions[] = $this->buildChangeClassAction();
// }
if (user_can('user-delete')) {
$actions[] = $this->buildDeleteAction();
}
@@ -101,10 +91,9 @@ class UserProfile extends ViewRecord
} elseif ($data['action'] == 'disable') {
$userRep->disableUser(Auth::user(), $data['uid'], $data['reason']);
}
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $data['uid']);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -118,10 +107,9 @@ class UserProfile extends ViewRecord
$userRep = $this->getRep();
try {
$userRep->removeTwoStepAuthentication(Auth::user(), $this->record->id);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -175,10 +163,9 @@ class UserProfile extends ViewRecord
} else {
$userRep->incrementDecrement(Auth::user(), $this->record->id, $data['action'], $data['field'], $data['value'], $data['reason']);
}
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -198,10 +185,9 @@ class UserProfile extends ViewRecord
$userRep = $this->getRep();
try {
$userRep->resetPassword($this->record->id, $data['password'], $data['password_confirmation']);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -223,10 +209,9 @@ class UserProfile extends ViewRecord
$examRep = new ExamRepository();
try {
$examRep->assignToUser($this->record->id, $data['exam_id'], $data['begin'], $data['end']);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -251,10 +236,9 @@ class UserProfile extends ViewRecord
$medalRep = new MedalRepository();
try {
$medalRep->grantToUser($this->record->id, $data['medal_id'], $data['duration']);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -272,8 +256,7 @@ class UserProfile extends ViewRecord
$this->record->status = User::STATUS_CONFIRMED;
$this->record->info= null;
$this->record->save();
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
});
}
@@ -287,10 +270,9 @@ class UserProfile extends ViewRecord
$userRep = $this->getRep();
try {
$userRep->updateDownloadPrivileges(Auth::user(), $this->record->id, $this->record->downloadpos == 'yes' ? 'no' : 'yes');
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
@@ -311,15 +293,14 @@ class UserProfile extends ViewRecord
$rep = $this->getRep();
try {
$rep->addMeta($this->record, $data, $data);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
private function buildDeleteAction(): Actions\Action
private function buildDeleteAction(): Actions\DeleteAction
{
return Actions\DeleteAction::make()->using(function () {
$this->getRep()->destroy($this->record->id);
@@ -345,7 +326,7 @@ class UserProfile extends ViewRecord
$props = [];
foreach ($metaList as $metaKey => $metas) {
$meta = $metas->first();
$text = sprintf('[%s]', $meta->metaKeyText, );
$text = sprintf('[%s]', $meta->metaKeyText);
if ($meta->meta_key == UserMeta::META_KEY_PERSONALIZED_USERNAME) {
$text .= sprintf('(%s)', $meta->getDeadlineText());
}
@@ -397,11 +378,28 @@ class UserProfile extends ViewRecord
$userRep = $this->getRep();
try {
$userRep->changeClass(Auth::user(), $this->record, $data['class'], $data['reason'], $data);
$this->notify('success', 'Success!');
$this->emitSelf(self::EVENT_RECORD_UPDATED, $this->record->id);
$this->sendSuccessNotification();
} catch (\Exception $exception) {
$this->notify('danger', $exception->getMessage());
$this->sendFailNotification($exception->getMessage());
}
});
}
private function sendSuccessNotification(string $msg = ""): void
{
Notification::make()
->success()
->title($msg ?: "Success!")
->send()
;
}
private function sendFailNotification(string $msg = ""): void
{
Notification::make()
->danger()
->title($msg ?: "Fail!")
->send()
;
}
}
+7
View File
@@ -11,6 +11,13 @@ 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;
+5 -5
View File
@@ -66,27 +66,27 @@ class Claim extends NexusModel
return Setting::get('torrent.claim_enabled', 'no') == 'yes';
}
public static function getConfigTorrentTTL()
public static function getConfigTorrentTTL(): int
{
return Setting::get('torrent.claim_torrent_ttl', self::TORRENT_TTL);
}
public static function getConfigUserUpLimit()
public static function getConfigUserUpLimit(): int
{
return Setting::get('torrent.claim_torrent_user_counts_up_limit', self::USER_UP_LIMIT);
}
public static function getConfigTorrentUpLimit()
public static function getConfigTorrentUpLimit(): int
{
return Setting::get('torrent.claim_user_torrent_counts_up_limit', self::TORRENT_UP_LIMIT);
}
public static function getConfigRemoveDeductBonus()
public static function getConfigRemoveDeductBonus(): int
{
return Setting::get('torrent.claim_remove_deduct_user_bonus', self::REMOVE_DEDUCT);
}
public static function getConfigGiveUpDeductBonus()
public static function getConfigGiveUpDeductBonus(): int
{
return Setting::get('torrent.claim_give_up_deduct_user_bonus', self::GIVE_UP_DEDUCT);
}
+1 -1
View File
@@ -73,7 +73,7 @@ class HitAndRun extends NexusModel
return '---';
}
$inspectTime = HitAndRun::getConfig('inspect_time', $searchBoxId);
$diffInSeconds = Carbon::now()->diffInSeconds($this->created_at->addHours($inspectTime));
$diffInSeconds = Carbon::now()->diffInSeconds($this->created_at->addHours(intval($inspectTime)));
return mkprettytime($diffInSeconds);
}
+5
View File
@@ -333,6 +333,11 @@ class User extends Authenticatable implements FilamentUser, HasName
);
}
protected function getTwoFactorAuthenticationStatusAttribute(): string
{
return $this->two_step_secret != "" ? "yes" : "no";
}
public static function getMinSeedPoints($class)
{
$setting = Setting::get("account.{$class}_min_seed_points");
+38 -7
View File
@@ -17,7 +17,11 @@ use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Route;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Table;
use Livewire\Livewire;
class AppPanelProvider extends PanelProvider
{
@@ -26,6 +30,11 @@ class AppPanelProvider extends PanelProvider
return $panel
->default()
->id('app')
->homeUrl("/")
->sidebarWidth("15rem")
->topbar(true)
->sidebarCollapsibleOnDesktop(true)
->authGuard("nexus-web")
->path('nexusphp')
->login()
->colors([
@@ -34,26 +43,48 @@ class AppPanelProvider extends PanelProvider
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
Pages\Dashboard::class,
// Pages\Dashboard::class,
\App\Filament\Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
// Widgets\AccountWidget::class,
// Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
// EncryptCookies::class,
\App\Http\Middleware\EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
// AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
\App\Http\Middleware\Locale::class,
])
->authMiddleware([
Authenticate::class,
\App\Http\Middleware\Filament::class,
]);
}
}
public function boot()
{
Table::configureUsing(function (Table $table): void {
$table
->filtersLayout(FiltersLayout::AboveContent)
->paginationPageOptions([10, 25, 50, 100])
;
});
}
public function register(): void
{
parent::register(); // TODO: Change the autogenerated stub
Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle)->middleware('filament');
});
}
}
-11
View File
@@ -1,11 +0,0 @@
<?php
namespace App\Providers;
class GoogleDriveAdapter extends \Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter
{
public function getService()
{
return $this->service;
}
}
+2 -1
View File
@@ -24,6 +24,7 @@
"ext-curl": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-mysqli": "*",
@@ -31,7 +32,7 @@
"ext-redis": "*",
"ext-xml": "*",
"ext-zend-opcache": "*",
"doctrine/dbal": "^3.1",
"ext-zip": "*",
"elasticsearch/elasticsearch": "^7.16",
"filament/filament": "^3.2",
"flowframe/laravel-trend": "^0.3",
Generated
+4 -2
View File
@@ -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": "e1e1ee1af8a78722edeedb53201ff4f3",
"content-hash": "11ad77956fab261081967fcfef0872f7",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -13513,13 +13513,15 @@
"ext-curl": "*",
"ext-gd": "*",
"ext-gmp": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-mysqli": "*",
"ext-pcntl": "*",
"ext-redis": "*",
"ext-xml": "*",
"ext-zend-opcache": "*"
"ext-zend-opcache": "*",
"ext-zip": "*"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
+108 -107
View File
@@ -3,157 +3,158 @@
return [
/*
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
| Class Namespace
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
|
| This value sets the root namespace for Livewire component classes in
| your application. This value affects component auto-discovery and
| any Livewire file helper commands, like `artisan make:livewire`.
|
| After changing this item, run: `php artisan livewire:discover`.
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Http\\Livewire',
'class_namespace' => 'App\\Livewire',
/*
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
| View Path
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
|
| This value sets the path for Livewire component views. This affects
| file manipulation helper commands like `artisan make:livewire`.
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
| Layout
|--------------------------------------------------------------------------
| The default layout view that will be used when rendering a component via
| Route::get('/some-endpoint', SomeComponent::class);. In this case the
| the view returned by SomeComponent will be wrapped in "layouts.app"
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|
*/
'layout' => 'layouts.app',
'layout' => 'components.layouts.app',
/*
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'asset_url' => null,
'lazy_placeholder' => null,
/*
|--------------------------------------------------------------------------
| Livewire App URL
|--------------------------------------------------------------------------
|
| This value should be used if livewire assets are served from CDN.
| Livewire will communicate with an app through this url.
|
| Examples: "https://my-app.com", "myurl.com/app".
|
*/
'app_url' => null,
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
// 'middleware_group' => 'web',
'middleware_group' => 'filament',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is validated and stored permanently. All file uploads
| are directed to a global endpoint for temporary storage. The config
| items below are used for customizing the way the endpoint works.
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' Default 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs.
'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
],
/*
|--------------------------------------------------------------------------
| Manifest File Path
|--------------------------------------------------------------------------
|
| This value sets the path to the Livewire manifest file.
| The default should work for most cases (which is
| "<app_root>/bootstrap/cache/livewire-components.php"), but for specific
| cases like when hosting on Laravel Vapor, it could be set to a different value.
|
| Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
|
*/
'manifest_path' => null,
/*
|--------------------------------------------------------------------------
| Back Button Cache
|--------------------------------------------------------------------------
|
| This value determines whether the back button cache will be used on pages
| that contain Livewire. By disabling back button cache, it ensures that
| the back button shows the correct state of components, instead of
| potentially stale, cached data.
|
| Setting it to "false" (default) will disable back button cache.
|
*/
'back_button_cache' => false,
/*
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
| Render On Redirect
|--------------------------------------------------------------------------
|---------------------------------------------------------------------------
|
| This value determines whether Livewire will render before it's redirected
| or not. Setting it to "false" (default) will mean the render method is
| skipped when redirecting. And "true" will mean the render method is
| run before redirecting. Browsers bfcache can store a potentially
| stale view if render is skipped on redirect.
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
];
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('attachments', function (Blueprint $table) {
$table->string('driver')->default("local");
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('attachments', function (Blueprint $table) {
$table->dropColumn('driver');
});
}
};
+2 -2
View File
@@ -1,6 +1,6 @@
<?php
defined('VERSION_NUMBER') || define('VERSION_NUMBER', '1.8.15');
defined('RELEASE_DATE') || define('RELEASE_DATE', '2024-12-23');
defined('VERSION_NUMBER') || define('VERSION_NUMBER', '1.9.0');
defined('RELEASE_DATE') || define('RELEASE_DATE', '2024-12-29');
defined('IN_TRACKER') || define('IN_TRACKER', false);
defined('PROJECTNAME') || define("PROJECTNAME","NexusPHP");
defined('NEXUSPHPURL') || define("NEXUSPHPURL","https://nexusphp.org");
+21 -16
View File
@@ -148,16 +148,19 @@ function print_attachment($dlkey, $enableimage = true, $imageresizer = true)
if ($row['isimage'] == 1)
{
if ($enableimage){
if ($row['thumb'] == 1){
$url = $httpdirectory_attachment."/".$row['location'].".thumb.jpg";
}
else{
$url = $httpdirectory_attachment."/".$row['location'];
}
// if ($row['thumb'] == 1){
// $url = $httpdirectory_attachment."/".$row['location'].".thumb.jpg";
// $url = $httpdirectory_attachment."/".$row['location'];
// }
// else{
// $url = $httpdirectory_attachment."/".$row['location'];
// }
$url = \Nexus\Attachment\Storage::getDriver($row['driver'])->getImageUrl($row['location']);
do_log(sprintf("driver: %s, location: %s, url: %s", $row['driver'], $row['location'], $url));
if($imageresizer == true)
$onclick = " onclick=\"Previewurl('".$httpdirectory_attachment."/".$row['location']."')\"";
$onclick = " onclick=\"Previewurl('".$url."')\"";
else $onclick = "";
$return = "<img id=\"attach".$id."\" alt=\"".htmlspecialchars($row['filename'])."\" src=\"".$url."\"". $onclick . " onmouseover=\"domTT_activate(this, event, 'content', '".htmlspecialchars("<strong>".$lang_functions['text_size']."</strong>: ".mksize($row['filesize'])."<br />".gettime($row['added']))."', 'styleClass', 'attach', 'x', findPosition(this)[0], 'y', findPosition(this)[1]-58);\" />";
$return = "<img id=\"attach".$id."\" style=\"max-width: 700px\" alt=\"".htmlspecialchars($row['filename'])."\" src=\"".$url."\"". $onclick . " onmouseover=\"domTT_activate(this, event, 'content', '".htmlspecialchars("<strong>".$lang_functions['text_size']."</strong>: ".mksize($row['filesize'])."<br />".gettime($row['added']))."', 'styleClass', 'attach', 'x', findPosition(this)[0], 'y', findPosition(this)[1]-58);\" />";
}
else $return = "";
}
@@ -345,13 +348,6 @@ function format_comment($text, $strip_html = true, $xssclean = false, $newtab =
$replaceXhtmlTagArray = array("<font face=\"\\1\">", "<span style=\"color: \\1;\">", "<span style=\"color: \\1;\">", "<font size=\"\\1\">");
$s = preg_replace($originalBbTagArray, $replaceXhtmlTagArray, $s);
if ($enableattach_attachment == 'yes' && $imagenum != 1){
$limit = 20;
// $s = preg_replace("/\[attach\]([0-9a-zA-z][0-9a-zA-z]*)\[\/attach\]/ies", "print_attachment('\\1', ".($enableimage ? 1 : 0).", ".($imageresizer ? 1 : 0).")", $s, $limit);
$s = preg_replace_callback("/\[attach\]([0-9a-zA-z][0-9a-zA-z]*)\[\/attach\]/is", function ($matches) use ($enableimage, $imageresizer) {
return print_attachment($matches[1], ".($enableimage ? 1 : 0).", ".($imageresizer ? 1 : 0).");
}, $s, $limit);
}
if ($enableimage) {
// $s = preg_replace("/\[img\]([^\<\r\n\"']+?)\[\/img\]/ei", "formatImg('\\1',".$imageresizer.",".$image_max_width.",".$image_max_height.")", $s, $imagenum, $imgReplaceCount);
@@ -451,6 +447,14 @@ function format_comment($text, $strip_html = true, $xssclean = false, $newtab =
}, $s);
}
if ($enableattach_attachment == 'yes' && $imagenum != 1){
$limit = 20;
// $s = preg_replace("/\[attach\]([0-9a-zA-z][0-9a-zA-z]*)\[\/attach\]/ies", "print_attachment('\\1', ".($enableimage ? 1 : 0).", ".($imageresizer ? 1 : 0).")", $s, $limit);
$s = preg_replace_callback("/\[attach\]([0-9a-zA-z][0-9a-zA-z]*)\[\/attach\]/is", function ($matches) use ($enableimage, $imageresizer) {
return print_attachment($matches[1], ".($enableimage ? 1 : 0).", ".($imageresizer ? 1 : 0).");
}, $s, $limit);
}
reset($tempCode);
$j = $i = 0;
while(count($tempCode) || $j > 5) {
@@ -5626,7 +5630,8 @@ function format_description($description)
if ($attachments->isNotEmpty()) {
$description = preg_replace_callback($pattern, function ($matches) use ($attachments) {
$item = $attachments->get($matches[2]);
$url = attachmentUrl($item->location);
$url = \Nexus\Attachment\Storage::getDriver($item->driver)->getImageUrl($item->location);
do_log(sprintf("location: %s, driver: %s, url: %s", $item->location, $item->driver, $url));
return str_replace($matches[2], $url, $matches[1]);
}, $description);
}
+8
View File
@@ -1337,3 +1337,11 @@ function get_user_locale(int $uid): string
}
return \App\Http\Middleware\Locale::$languageMaps[$result[0]['site_lang_folder']] ?? 'en';
}
function send_admin_success_notification(string $msg = ""): void {
\Filament\Notifications\Notification::make()->success()->title($msg ?: "Success!")->send();
}
function send_admin_fail_notification(string $msg = ""): void {
\Filament\Notifications\Notification::make()->danger()->title($msg ?: "Fail!")->send();
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Nexus\Attachment\Drivers;
use GuzzleHttp\Psr7;
use Nexus\Attachment\Storage;
class Chevereto extends Storage {
function upload(string $filepath): string
{
$api = get_setting("image_hosting_chevereto.upload_api_endpoint");
$token = get_setting("image_hosting_chevereto.upload_token");
$logPrefix = "filepath: $filepath, api: $api, token: $token";
$httpClient = new \GuzzleHttp\Client();
$response = $httpClient->request('POST', $api, [
'headers' => [
'X-API-Key' => sprintf('%s', $token),
],
'multipart' => [
[
'name' => 'source',
'contents' => Psr7\Utils::tryFopen($filepath, 'r')
]
]
]);
$statusCode = $response->getStatusCode();
$logPrefix .= ", status code: $statusCode";
if ($statusCode != 200) {
do_log("$logPrefix, statusCode != 200", "error");
throw new \Exception("Unable to upload file, status code {$statusCode}");
}
$stringBody = (string)$response->getBody();
$logPrefix .= ", body: $stringBody";
$result = json_decode($stringBody, true);
if (!is_array($result)) {
do_log("$logPrefix, can not parse to array", "error");
throw new \Exception("Unable to parse response body");
}
if (!isset($result["image"]["url"])) {
do_log("$logPrefix, no image url", "error");
throw new \Exception("upload fail: " . ($result["error"]["message"] ?? ""));
}
return $result["image"]["url"];
}
function getBaseUrl(): string
{
return get_setting("image_hosting_chevereto.base_url");
}
function getDriverName(): string
{
return static::DRIVER_CHEVERETO;
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Nexus\Attachment\Drivers;
use Nexus\Attachment\Storage;
class Local extends Storage {
function upload(string $filepath): string
{
throw new \RuntimeException("Not implemented");
}
function getBaseUrl(): string
{
return sprintf("%s/%s", getSchemeAndHttpHost(), trim(get_setting("attachment.httpdirectory"), '/'));
}
function getDriverName(): string
{
return static::DRIVER_LOCAL;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Nexus\Attachment\Drivers;
use GuzzleHttp\Psr7;
use Nexus\Attachment\Storage;
class Lsky extends Storage {
function upload(string $filepath): string
{
$api = get_setting("image_hosting_lsky.upload_api_endpoint");
$token = get_setting("image_hosting_lsky.upload_token");
$logPrefix = "filepath: $filepath, api: $api, token: $token";
$httpClient = new \GuzzleHttp\Client();
$response = $httpClient->request('POST', $api, [
'headers' => [
'Authorization' => sprintf('Bearer %s', $token),
],
'multipart' => [
[
'name' => 'file',
'contents' => Psr7\Utils::tryFopen($filepath, 'r')
]
]
]);
$statusCode = $response->getStatusCode();
$logPrefix .= ", status code: $statusCode";
if ($statusCode != 200) {
do_log("$logPrefix, statusCode != 200", "error");
throw new \Exception("Unable to upload file, status code {$statusCode}");
}
$stringBody = (string)$response->getBody();
$logPrefix .= ", body: $stringBody";
$result = json_decode($stringBody, true);
if (!is_array($result)) {
do_log("$logPrefix, can not parse to array", "error");
throw new \Exception("Unable to parse response body");
}
if (!isset($result["status"])) {
do_log("$logPrefix, no status", "error");
throw new \Exception("Unable to parse response body, no status");
}
if ($result["status"] !== true) {
do_log("$logPrefix, status != true", "error");
throw new \Exception("upload fail: " . $result["message"]);
}
if (!isset($result["data"]["links"]["url"])) {
do_log("$logPrefix, no links url", "error");
throw new \Exception("upload fail: no links url");
}
return $result["data"]["links"]["url"];
}
function getBaseUrl(): string
{
return get_setting("image_hosting_lsky.base_url");
}
function getDriverName(): string
{
return static::DRIVER_LSKY;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Nexus\Attachment;
use Nexus\Attachment\Drivers\Chevereto;
use Nexus\Attachment\Drivers\Local;
use Nexus\Attachment\Drivers\Lsky;
abstract class Storage {
private static array $drivers = [];
const DRIVER_LOCAL = 'local';
const DRIVER_CHEVERETO = 'chevereto';
const DRIVER_LSKY = 'lsky';
/**
* upload to remote and return full url
*
* @param string $filepath
* @return string
*/
abstract function upload(string $filepath): string;
abstract function getBaseUrl(): string;
abstract function getDriverName(): string;
public function uploadGetLocation(string $filepath): string
{
$url = $this->upload($filepath);
return $this->trimBaseUrl($url);
}
public function getImageUrl(string $location): string
{
return sprintf('%s/%s', trim($this->getBaseUrl(), '/'), trim($location, '/'));
}
protected function trimBaseUrl(string $url): string
{
$baseUrl = trim($this->getBaseUrl(), '/') . "/";
if (str_starts_with($url, $baseUrl)) {
return substr($url, strlen($baseUrl));
}
return $url;
}
public static function getDriver(string $name = null): Storage
{
$driver = $name ?: get_setting("image_hosting.driver");
if (isset(self::$drivers[$driver])) {
return self::$drivers[$driver];
}
$result = null;
if ($driver == self::DRIVER_CHEVERETO) {
$result = new Chevereto();
} else if ($driver == self::DRIVER_LSKY) {
$result = new Lsky();
} else if ($driver == self::DRIVER_LOCAL) {
$result = new Local();
}
if ($result) {
return self::$drivers[$driver] = $result;
}
throw new \Exception("Unsupported driver: $driver");
}
}
-1
View File
@@ -260,7 +260,6 @@ class NexusDB
$capsule->bootEloquent();
$connection = self::$eloquentConnection = $capsule->getConnection($connectionName);
$connection->enableQueryLog();
$connection->getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
}
private static function schema(): \Illuminate\Database\Schema\Builder
+188 -171
View File
@@ -58,183 +58,200 @@ if ($Attach->enable_attachment())
if (in_array($ext, $img_ext))
$isimage = true;
else $isimage = false;
if ($savedirectorytype_attachment == 'onedir')
$savepath = "";
elseif ($savedirectorytype_attachment == 'monthdir')
$savepath = date("Ym")."/";
elseif ($savedirectorytype_attachment == 'daydir')
$savepath = date("Ymd")."/";
$filemd5 = md5_file($file['tmp_name']);
$filename = date("YmdHis").$filemd5;
$file_location = make_folder($savedirectory_attachment."/", $savepath) . $filename;
do_log("file_location: $file_location");
$db_file_location = $savepath.$filename;
$abandonorig = false;
$hasthumb = false;
$width = 0;
if ($isimage) //the uploaded file is a image
{
$maycreatethumb = false;
$stop = false;
$imagesize = getimagesize($file['tmp_name']);
if ($imagesize){
$height = $imagesize[1];
$width = $imagesize[0];
$it = $imagesize[2];
if ($it != 1 || !$Attach->is_gif_ani($file['tmp_name'])){ //if it is an animation GIF, stop creating thumbnail and adding watermark
if ($thumbnailtype_attachment != 'no') //create thumbnail for big image
{
//determine the size of thumbnail
if ($altsize == 'yes'){
$targetwidth = $altthumbwidth_attachment;
$targetheight = $altthumbheight_attachment;
}
else
{
$targetwidth = $thumbwidth_attachment;
$targetheight = $thumbheight_attachment;
}
$hscale=$height/$targetheight;
$wscale=$width/$targetwidth;
$scale=($hscale < 1 && $wscale < 1) ? 1 : (( $hscale > $wscale) ? $hscale : $wscale);
$newwidth=floor($width/$scale);
$newheight=floor($height/$scale);
if ($scale != 1){ //thumbnail is needed
if ($it==1)
$orig=@imagecreatefromgif($file["tmp_name"]);
elseif ($it == 2)
$orig=@imagecreatefromjpeg($file["tmp_name"]);
else
$orig=@imagecreatefrompng($file["tmp_name"]);
if ($orig && !$stop)
{
$thumb = imagecreatetruecolor($newwidth, $newheight);
imagecopyresampled($thumb, $orig, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
if ($thumbnailtype_attachment == 'createthumb'){
$hasthumb = true;
imagejpeg($thumb, $file_location.".".$ext.".thumb.jpg", $thumbquality_attachment);
}
elseif ($thumbnailtype_attachment == 'resizebigimg'){
$ext = "jpg";
$filetype = "image/jpeg";
$it = 2;
$height = $newheight;
$width = $newwidth;
$maycreatethumb = true;
$abandonorig = true;
}
}
}
}
$watermarkpos = $watermarkpos_attachment;
if ($watermarkpos != 'no' && !$stop) //add watermark to image
{
if ($width > $watermarkwidth_attachment && $height > $watermarkheight_attachment)
{
if ($abandonorig)
{
$resource = $thumb;
}
else
{
$resource=imagecreatetruecolor($width,$height);
if ($it==1)
$resource_p=@imagecreatefromgif($file["tmp_name"]);
elseif ($it==2)
$resource_p=@imagecreatefromjpeg($file["tmp_name"]);
else
$resource_p=@imagecreatefrompng($file["tmp_name"]);
imagecopy($resource, $resource_p, 0, 0, 0, 0, $width, $height);
}
$watermark = imagecreatefrompng('pic/watermark.png');
$watermark_width = imagesx($watermark);
$watermark_height = imagesy($watermark);
//the position of the watermark
if ($watermarkpos == 'random')
$watermarkpos = mt_rand(1, 9);
switch ($watermarkpos)
{
case 1: {
$wmx = 5;
$wmy = 5;
break;
}
case 2: {
$wmx = ($width-$watermark_width)/2;
$wmy = 5;
break;
}
case 3: {
$wmx = $width-$watermark_width-5;
$wmy = 5;
break;
}
case 4: {
$wmx = 5;
$wmy = ($height-$watermark_height)/2;
break;
}
case 5: {
$wmx = ($width-$watermark_width)/2;
$wmy = ($height-$watermark_height)/2;
break;
}
case 6: {
$wmx = $width-$watermark_width-5;
$wmy = ($height-$watermark_height)/2;
break;
}
case 7: {
$wmx = 5;
$wmy = $height-$watermark_height-5;
break;
}
case 8: {
$wmx = ($width-$watermark_width)/2;
$wmy = $height-$watermark_height-5;
break;
}
case 9: {
$wmx = $width-$watermark_width-5;
$wmy = $height-$watermark_height-5;
break;
}
}
$width = $height = 0;
$imagesize = [];
if ($isimage) {
$imagesize = getimagesize($file['tmp_name']);
$height = $imagesize[1];
$width = $imagesize[0];
}
//get driver
$storageDriver = get_setting("image_hosting.driver", "local");
if ($storageDriver == "local" || !$isimage) {
if ($savedirectorytype_attachment == 'onedir')
$savepath = "";
elseif ($savedirectorytype_attachment == 'monthdir')
$savepath = date("Ym")."/";
elseif ($savedirectorytype_attachment == 'daydir')
$savepath = date("Ymd")."/";
$filemd5 = md5_file($file['tmp_name']);
$filename = date("YmdHis").$filemd5;
$file_location = make_folder($savedirectory_attachment."/", $savepath) . $filename;
do_log("file_location: $file_location");
$db_file_location = $savepath.$filename;
$abandonorig = false;
$hasthumb = false;
if ($isimage) //the uploaded file is a image
{
$maycreatethumb = false;
$stop = false;
if ($imagesize){
$it = $imagesize[2];
if ($it != 1 || !$Attach->is_gif_ani($file['tmp_name'])){ //if it is an animation GIF, stop creating thumbnail and adding watermark
if ($thumbnailtype_attachment != 'no') //create thumbnail for big image
{
//determine the size of thumbnail
if ($altsize == 'yes'){
$targetwidth = $altthumbwidth_attachment;
$targetheight = $altthumbheight_attachment;
}
else
{
$targetwidth = $thumbwidth_attachment;
$targetheight = $thumbheight_attachment;
}
$hscale=$height/$targetheight;
$wscale=$width/$targetwidth;
$scale=($hscale < 1 && $wscale < 1) ? 1 : (( $hscale > $wscale) ? $hscale : $wscale);
$newwidth=floor($width/$scale);
$newheight=floor($height/$scale);
if ($scale != 1){ //thumbnail is needed
if ($it==1)
$orig=@imagecreatefromgif($file["tmp_name"]);
elseif ($it == 2)
$orig=@imagecreatefromjpeg($file["tmp_name"]);
else
$orig=@imagecreatefrompng($file["tmp_name"]);
if ($orig && !$stop)
{
$thumb = imagecreatetruecolor($newwidth, $newheight);
imagecopyresampled($thumb, $orig, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
if ($thumbnailtype_attachment == 'createthumb'){
$hasthumb = true;
imagejpeg($thumb, $file_location.".".$ext.".thumb.jpg", $thumbquality_attachment);
}
elseif ($thumbnailtype_attachment == 'resizebigimg'){
$ext = "jpg";
$filetype = "image/jpeg";
$it = 2;
$height = $newheight;
$width = $newwidth;
$maycreatethumb = true;
$abandonorig = true;
}
}
}
}
$watermarkpos = $watermarkpos_attachment;
if ($watermarkpos != 'no' && !$stop) //add watermark to image
{
if ($width > $watermarkwidth_attachment && $height > $watermarkheight_attachment)
{
if ($abandonorig)
{
$resource = $thumb;
}
else
{
$resource=imagecreatetruecolor($width,$height);
if ($it==1)
$resource_p=@imagecreatefromgif($file["tmp_name"]);
elseif ($it==2)
$resource_p=@imagecreatefromjpeg($file["tmp_name"]);
else
$resource_p=@imagecreatefrompng($file["tmp_name"]);
imagecopy($resource, $resource_p, 0, 0, 0, 0, $width, $height);
}
$watermark = imagecreatefrompng('pic/watermark.png');
$watermark_width = imagesx($watermark);
$watermark_height = imagesy($watermark);
//the position of the watermark
if ($watermarkpos == 'random')
$watermarkpos = mt_rand(1, 9);
switch ($watermarkpos)
{
case 1: {
$wmx = 5;
$wmy = 5;
break;
}
case 2: {
$wmx = ($width-$watermark_width)/2;
$wmy = 5;
break;
}
case 3: {
$wmx = $width-$watermark_width-5;
$wmy = 5;
break;
}
case 4: {
$wmx = 5;
$wmy = ($height-$watermark_height)/2;
break;
}
case 5: {
$wmx = ($width-$watermark_width)/2;
$wmy = ($height-$watermark_height)/2;
break;
}
case 6: {
$wmx = $width-$watermark_width-5;
$wmy = ($height-$watermark_height)/2;
break;
}
case 7: {
$wmx = 5;
$wmy = $height-$watermark_height-5;
break;
}
case 8: {
$wmx = ($width-$watermark_width)/2;
$wmy = $height-$watermark_height-5;
break;
}
case 9: {
$wmx = $width-$watermark_width-5;
$wmy = $height-$watermark_height-5;
break;
}
}
imagecopy($resource, $watermark, $wmx, $wmy, 0, 0, $watermark_width, $watermark_height);
if ($it==1)
imagegif($resource, $file_location.".".$ext);
elseif ($it==2)
imagejpeg($resource, $file_location.".".$ext, $watermarkquality_attachment);
else
imagepng($resource, $file_location.".".$ext);
$filesize = filesize($file_location.".".$ext);
$maycreatethumb = false;
$abandonorig = true;
}
}
if ($maycreatethumb){ // if no watermark is added, create the thumbnail now for the above resized image.
imagejpeg($thumb, $file_location.".".$ext, $thumbquality_attachment);
$filesize = filesize($file_location.".".$ext);
}
}
}
else $warning = $lang_attachment['text_invalid_image_file'];
}
if (!$abandonorig){
if(!move_uploaded_file($file["tmp_name"], $file_location.".".$ext))
$warning = $lang_attachment['text_cannot_move_file'];
}
imagecopy($resource, $watermark, $wmx, $wmy, 0, 0, $watermark_width, $watermark_height);
if ($it==1)
imagegif($resource, $file_location.".".$ext);
elseif ($it==2)
imagejpeg($resource, $file_location.".".$ext, $watermarkquality_attachment);
else
imagepng($resource, $file_location.".".$ext);
$filesize = filesize($file_location.".".$ext);
$maycreatethumb = false;
$abandonorig = true;
}
}
if ($maycreatethumb){ // if no watermark is added, create the thumbnail now for the above resized image.
imagejpeg($thumb, $file_location.".".$ext, $thumbquality_attachment);
$filesize = filesize($file_location.".".$ext);
}
}
}
else $warning = $lang_attachment['text_invalid_image_file'];
}
if (!$abandonorig){
if(!move_uploaded_file($file["tmp_name"], $file_location.".".$ext))
$warning = $lang_attachment['text_cannot_move_file'];
}
$url = $httpdirectory_attachment."/".$db_file_location . ".$ext";
if ($hasthumb) {
$url .= ".thumb.jpg";
}
} else {
try {
$driver = \Nexus\Attachment\Storage::getDriver();
$location = $driver->uploadGetLocation($file["tmp_name"]);
$db_file_location = substr($location, 0, -1*strlen($ext)-1);
$url = $driver->getImageUrl($location);
} catch (\Exception $exception) {
do_log("upload failed: " . $exception->getMessage() . $exception->getTraceAsString(), 'error');
$warning = $exception->getMessage();
}
}
if (!$warning) //insert into database and add code to editor
{
$dlkey = md5($db_file_location.".".$ext);
sql_query("INSERT INTO attachments (userid, width, added, filename, filetype, filesize, location, dlkey, isimage, thumb) VALUES (".$CURUSER['id'].", ".$width.", ".sqlesc(date("Y-m-d H:i:s")).", ".sqlesc($origfilename).", ".sqlesc($filetype).", ".$filesize.", ".sqlesc($db_file_location.".".$ext).", ".sqlesc($dlkey).", ".($isimage ? 1 : 0).", ".($hasthumb ? 1 : 0).")") or sqlerr(__FILE__, __LINE__);
sql_query("INSERT INTO attachments (userid, width, added, filename, filetype, filesize, location, dlkey, isimage, thumb, driver) VALUES (".$CURUSER['id'].", ".$width.", ".sqlesc(date("Y-m-d H:i:s")).", ".sqlesc($origfilename).", ".sqlesc($filetype).", ".$filesize.", ".sqlesc($db_file_location.".".$ext).", ".sqlesc($dlkey).", ".($isimage ? 1 : 0).", ".($hasthumb ? 1 : 0). "," .sqlesc($storageDriver) . ")") or sqlerr(__FILE__, __LINE__);
$count_left--;
if (!empty($_REQUEST['callback_func']) && preg_match('/^preview_custom_field_image_\d+$/', $_REQUEST['callback_func'])) {
$url = $httpdirectory_attachment."/".$db_file_location . ".$ext";
if ($hasthumb) {
$url .= ".thumb.jpg";
}
echo sprintf('<script type="text/javascript">parent.%s("%s", "%s")</script>', $_REQUEST['callback_func'], $dlkey, $url);
} else {
echo("<script type=\"text/javascript\">parent.tag_extimage('". "[attach]" . $dlkey . "[/attach]" . "');</script>");
+1 -1
View File
@@ -120,7 +120,7 @@ if ($CURUSER['id'] == $user['id'] || user_can('cruprfmanage'))
<table width="100%" border="1" cellspacing="0" cellpadding="5">
<?php
$userIdDisplay = $user['id'];
$userManageSystemUrl = sprintf('%s/%s/users/%s',getSchemeAndHttpHost(), nexus_env('FILAMENT_PATH', 'nexusphp'), $user['id']);
$userManageSystemUrl = sprintf('%s/%s/user/users/%s',getSchemeAndHttpHost(), nexus_env('FILAMENT_PATH', 'nexusphp'), $user['id']);
$userManageSystemText = sprintf('<a href="%s" target="_blank" class="altlink">%s</a>', $userManageSystemUrl, $lang_functions['text_management_system']);
$migratedHelp = sprintf($lang_userdetails['change_field_value_migrated'], $userManageSystemText);
if (user_can('prfmanage') && $user["class"] < get_user_class()) {
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+10018 -14
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -1 +1,2 @@
{"/livewire.js":"/livewire.js?id=de3fca26689cb5a39af4"}
{"/livewire.js":"38dc8241"}
+9
View File
@@ -40,6 +40,7 @@ return [
'city' => 'City',
'client' => 'Client',
'reason' => 'Reason',
'change' => 'Change',
'setting' => [
'nav_text' => 'Setting',
'backup' => [
@@ -111,6 +112,14 @@ return [
'alarm_email_receiver' => 'Alarm email receiver',
'alarm_email_receiver_help' => "Fill in the UID of the user, separated by space, and the alarm email will be sent to the corresponding user's email address. If you don't fill it in, it will be written to the runtime log, and the log level will be error",
],
'image_hosting' => [
'driver' => 'Storage location',
'driver_help' => 'If you choose local, the default is to save it locally on the server where the website is located, otherwise upload it to the corresponding image server',
'tab_header' => 'Image hosting',
'upload_api_endpoint' => 'Upload interface address',
'base_url' => 'Image URL prefix',
'upload_token' => 'Upload token',
]
],
'user' => [
'label' => 'User',
+9
View File
@@ -40,6 +40,7 @@ return [
'city' => '城市',
'client' => '客户端',
'reason' => '原因',
'change' => '修改',
'setting' => [
'nav_text' => '设置',
'backup' => [
@@ -111,6 +112,14 @@ return [
'alarm_email_receiver' => '告警邮件接收者',
'alarm_email_receiver_help' => '填写用户 UID,多个空格隔开,系统异常告警邮件将会发到对应用户的邮箱。如果不填会写到运行日志中,日志级别为 error',
],
'image_hosting' => [
'driver' => '存储位置',
'driver_help' => '若选择 local, 对应默认的保存在网站所在服务器本地, 否则上传到对应的图片服务器',
'tab_header' => '图床',
'upload_api_endpoint' => '上传接口地址',
'base_url' => '图片 URL 前缀',
'upload_token' => '上传令牌',
]
],
'user' => [
'label' => '用户',
+9
View File
@@ -40,6 +40,7 @@ return [
'city' => '城市',
'client' => '客戶端',
'reason' => '原因',
'change' => '修改',
'setting' => [
'nav_text' => '設置',
'backup' => [
@@ -111,6 +112,14 @@ return [
'alarm_email_receiver' => '告警郵件接收者',
'alarm_email_receiver_help' => '填寫用戶 UID,多個空格隔開,系統異常告警郵件將會發到對應用戶的郵箱。如果不填會寫到運行日誌中,日誌級別為 error',
],
'image_hosting' => [
'driver' => '存儲位置',
'driver_help' => '若選擇 local, 對應默認的保存在網站所在服務器本地, 否則上傳到對應的圖片服務器',
'tab_header' => '圖床',
'upload_api_endpoint' => '上傳接口地址',
'base_url' => '圖片 URL 前綴',
'upload_token' => '上傳令牌',
]
],
'user' => [
'label' => '用戶',
@@ -3,7 +3,7 @@
{{ $this->form }}
<div class="flex justify-center mt-10" style="margin-top: 20px;">
<button type="submit" class="inline-flex items-center justify-center gap-1 font-medium rounded-lg border transition-colors focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset filament-button h-9 px-4 text-sm text-white shadow focus:ring-white border-transparent bg-primary-600 hover:bg-primary-500 focus:bg-primary-700 focus:ring-offset-primary-700 filament-page-button-action">
{{__('filament::resources/pages/edit-record.form.actions.save.label')}}
{{__('filament-actions::edit.single.modal.actions.save.label')}}
</button>
</div>
</form>
@@ -1,16 +1,21 @@
<x-filament::widget>
<x-filament::card>
@php
$user = \Filament\Facades\Filament::auth()->user();
@endphp
@php
$user = filament()->auth()->user();
@endphp
<div class="h-12 flex items-center space-x-4 rtl:space-x-reverse">
<div>
<h2 class="text-lg sm:text-xl font-bold tracking-tight">
{{ __('filament::widgets/account-widget.welcome', ['user' => \Filament\Facades\Filament::getUserName($user) . '(' . $user->classText . ')']) }}
<x-filament-widgets::widget class="fi-account-widget">
<x-filament::section>
<div class="flex items-center gap-x-3">
<div class="flex-1">
<h2
class="grid flex-1 text-base font-semibold leading-6 text-gray-950 dark:text-white"
>
{{ __('filament-panels::widgets/account-widget.welcome', ['app' => config('app.name')]) }},
{{ filament()->getUserName($user) . '(' . $user->classText . ')' }}
</h2>
</div>
</div>
</x-filament::card>
</x-filament::widget>
</x-filament::section>
</x-filament-widgets::widget>
@@ -1,36 +1,36 @@
<x-filament::widget class="filament-widgets-table-widget">
<div class="p-2 space-y-2 bg-white rounded-xl shadow">
<div class="space-y-2">
<div class="px-4 py-2 space-y-4">
<div class="flex items-center justify-between gap-8">
<h2 class="text-xl font-semibold tracking-tight filament-card-heading">
{{$header}}
</h2>
</div>
<div aria-hidden="true" class="border-t filament-hr"></div>
<div class="overflow-y-auto relative">
<table class="w-full text-left rtl:text-right divide-y table-auto filament-tables-table">
<tbody class="divide-y whitespace-nowrap">
@foreach(array_chunk($data, 2) as $chunk)
<tr class="filament-tables-row">
@foreach($chunk as $item)
<th class="filament-tables-cell"><div class="px-4 py-3 filament-tables-text-column">{{$item['text']}}</div></th>
<td class="filament-tables-cell"
<x-filament-widgets::widget class="fi-wi-table">
<div class="filament-widgets-card rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm p-6">
<!-- Header Section -->
<div class="fi-ta-header flex flex-col gap-3 p-4 sm:px-6 sm:flex-row sm:items-center">
<div class="grid gap-y-1">
<h3 class="fi-ta-header-heading text-base font-semibold leading-6 text-gray-950 dark:text-white">
{{ $header }}
</h3>
</div>
</div>
<!-- Table Section -->
<div class="fi-ta-content border-t relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10">
<table class="fi-ta-table w-full table-auto divide-y divide-gray-200 text-start dark:divide-white/5">
<tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
@foreach(array_chunk($data, 2) as $chunk)
<tr class="bg-white dark:bg-gray-800">
@foreach($chunk as $item)
<th class="fi-ta-header-cell px-3 py-3.5 sm:first-of-type:ps-6 sm:last-of-type:pe-6 fi-table-header-cell-id">
{{$item['text']}}
</th>
<td class="fi-ta-cell p-0 first-of-type:ps-1 last-of-type:pe-1 sm:first-of-type:ps-3 sm:last-of-type:pe-3 fi-table-cell-id"
@if($loop->count == 1)
colspan="3"
@endif
>
<div class="px-4 py-3 filament-tables-text-column {{$item['class'] ?? ''}}"><span class="">{{$item['value']}}</span></div>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
>
<div class="{{$item['class'] ?? ''}}">{{$item['value']}}</div>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</x-filament::widget>
</x-filament-widgets::widget>
+1 -1
View File
@@ -2,5 +2,5 @@
use Illuminate\Support\Facades\Route;
Route::post('nastools/approve', [\App\Http\Controllers\AuthenticateController::class, 'nasToolsApprove']);
Route::GET('iyuu/approve', [\App\Http\Controllers\AuthenticateController::class, 'iyuuApprove']);
Route::get('iyuu/approve', [\App\Http\Controllers\AuthenticateController::class, 'iyuuApprove']);