Compare commits

..

16 Commits

Author SHA1 Message Date
Boy132
e35ce1e79d Use folder name as id when having id mismatch to allow deletion (#2263) 2026-02-23 20:25:19 +01:00
Charles
42c127c004 Fix invisible force delete button (#2262) 2026-02-23 13:41:15 -05:00
Boy132
593f209142 Fix stock egg UUID migration (#2259) 2026-02-22 17:23:51 +01:00
Boy132
9bf5b2cf0a Do not throw error when checking for egg updates (#2256) 2026-02-22 17:23:09 +01:00
Lance Pioch
f76e864a30 Laravel 12.52.0 Shift (#2247)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-22 11:11:18 -05:00
Boy132
01cfa31ee1 Limit activity logs on profile page (#2253) 2026-02-19 14:57:48 +01:00
Boy132
dead664e4d Make plugin id in artisan commands also case insensitive (#2252) 2026-02-19 14:57:19 +01:00
Charles
1bbbcd0e25 Update server/egg icon url supported file types (#2249) 2026-02-18 16:30:24 -05:00
Michael (Parker) Parker
677d2f742c docker env fixes (#2234)
Co-authored-by: Charles <charles@pelican.dev>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-02-18 14:40:46 -05:00
Charles
650fb16d2d exclude bulk actions in list eggs (#2239) 2026-02-17 06:17:28 -05:00
Boy132
58814ea782 Allow to delete errored plugins (#2246) 2026-02-17 11:52:25 +01:00
hallo123wert
0918ed308b exclude bulk actions (#2240) 2026-02-16 06:34:14 -05:00
Lance Pioch
85d5f2ec3f Fix alert banners (#1492) (#2177) 2026-02-15 15:34:09 -05:00
Lance Pioch
810f237547 Remove ignition, just use new default error handler (#2241) 2026-02-15 15:03:55 -05:00
Lance Pioch
8f191890a1 Fix null backup limit exception (#2242) 2026-02-15 15:03:37 -05:00
Boy132
160e0e54f5 More action fixes (#2237)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-15 17:31:50 +01:00
46 changed files with 292 additions and 783 deletions

View File

@@ -69,8 +69,7 @@ RUN apk add --no-cache \
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public

View File

@@ -74,8 +74,7 @@ RUN apk add --no-cache \
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public

View File

@@ -8,7 +8,6 @@ use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use JsonException;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command
@@ -22,14 +21,12 @@ class CheckEggUpdatesCommand extends Command
try {
$this->check($egg, $exporterService);
} catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
$this->error("$egg->name: Error ({$exception->getMessage()})");
}
}
}
/**
* @throws JsonException
*/
/** @throws Exception */
private function check(Egg $egg, EggExporterService $exporterService): void
{
if (is_null($egg->update_url)) {
@@ -45,7 +42,13 @@ class CheckEggUpdatesCommand extends Command
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url)->throw()->body();
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url);
if ($remote->failed()) {
throw new Exception("HTTP request returned status code {$remote->status()}");
}
$remote = $remote->body();
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
unset($local['exported_at'], $remote['exported_at']);

View File

@@ -16,7 +16,7 @@ class DisablePluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');

View File

@@ -18,7 +18,7 @@ class InstallPluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');

View File

@@ -18,7 +18,7 @@ class UninstallPluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');

View File

@@ -17,7 +17,7 @@ class UpdatePluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions;
use App\Exceptions\Solutions\ManifestDoesNotExistSolution;
use Exception;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new ManifestDoesNotExistSolution();
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Exceptions\Solutions;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistSolution implements Solution
{
public function getSolutionTitle(): string
{
return "The manifest.json file hasn't been generated yet";
}
public function getSolutionDescription(): string
{
return 'Run yarn run build:production to build the frontend first.';
}
public function getDocumentationLinks(): array
{
return [
'Docs' => 'https://github.com/pelican/panel/blob/master/package.json',
];
}
}

View File

@@ -101,7 +101,7 @@ class DatabaseHostResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::Database)

View File

@@ -175,7 +175,8 @@ class CreateEgg extends CreateRecord
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)

View File

@@ -333,9 +333,9 @@ class EditEgg extends EditRecord
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
@@ -485,18 +485,20 @@ class EditEgg extends EditRecord
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
}

View File

@@ -86,7 +86,7 @@ class ListEggs extends ListRecords
->multiple(),
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make()
DeleteBulkAction::make('exclude_bulk_delete')
->before(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
@@ -106,7 +106,7 @@ class ListEggs extends ListRecords
$this->halt();
}
}),
UpdateEggBulkAction::make()
UpdateEggBulkAction::make('exclude_bulk_update')
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);

View File

@@ -104,7 +104,7 @@ class MountResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::LayersLinked)

View File

@@ -103,6 +103,7 @@ class AllocationsRelationManager extends RelationManager
->live()
->hintAction(
Action::make('hint_refresh')
->hiddenLabel()
->icon(TablerIcon::Refresh)
->tooltip(trans('admin/node.refresh'))
->action(function () {

View File

@@ -202,7 +202,7 @@ class PluginResource extends Resource
->icon(TablerIcon::Trash)
->color('danger')
->requiresConfirmation()
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->deletePlugin($plugin);

View File

@@ -106,7 +106,7 @@ class RoleResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0);

View File

@@ -121,6 +121,7 @@ class EditServer extends EditRecord
->columnSpan(2)
->alignJustify(),
Action::make('uploadIcon')
->hiddenLabel()
->icon(TablerIcon::PhotoUp)
->tooltip(trans('admin/server.import_image'))
->modal()
@@ -320,7 +321,7 @@ class EditServer extends EditRecord
try {
$logs = $serverRepository->setServer($server)->getInstallLogs();
return convert_to_utf8($logs);
return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']);
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
@@ -1126,7 +1127,7 @@ class EditServer extends EditRecord
->hidden(fn () => $canForceDelete)
->authorize(fn (Server $server) => user()?->can('delete server', $server))
->icon(TablerIcon::Trash),
Action::make('ForceDelete')
Action::make('exclude_force_delete')
->color('danger')
->label(trans('filament-actions::force-delete.single.label'))
->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
@@ -1218,18 +1219,20 @@ class EditServer extends EditRecord
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}
}

View File

@@ -114,7 +114,7 @@ class WebhookResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::Webhook)

View File

@@ -16,13 +16,15 @@ class RotateDatabasePasswordAction extends Action
{
public static function getDefaultName(): ?string
{
return 'hint_rotate';
return 'exclude_hint_rotate';
}
protected function setUp(): void
{
parent::setUp();
$this->hiddenLabel();
$this->tooltip(trans('admin/databasehost.rotate'));
$this->icon(TablerIcon::Refresh);

View File

@@ -426,13 +426,13 @@ class EditProfile extends BaseEditProfile
->label(trans('profile.tabs.activity'))
->icon(TablerIcon::History)
->schema([
Repeater::make('activity')
->hiddenLabel()
Repeater::make('activity') // TODO: move to a table
->label(trans('profile.activity_info'))
->inlineLabel(false)
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
$query->orderBy('timestamp', 'desc')->limit(50);
})
->schema([
TextEntry::make('log')

View File

@@ -462,18 +462,20 @@ class Settings extends ServerFormPage
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}

View File

@@ -76,7 +76,7 @@ class BackupResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
return $server->backup_limit;
return $server->backup_limit ?? 0;
}
public static function defaultForm(Schema $schema): Schema

View File

@@ -62,7 +62,7 @@ class DatabaseResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
return $server->database_limit;
return $server->database_limit ?? 0;
}
/**

View File

@@ -149,7 +149,7 @@ class EditFiles extends Page
try {
$contents = $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
return convert_to_utf8($contents);
return mb_convert_encoding($contents, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']);
} catch (FileSizeTooLargeException) {
AlertBanner::make('file_too_large')
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))

View File

@@ -7,6 +7,7 @@ use App\Facades\Activity;
use App\Models\Schedule;
use App\Models\Task;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
@@ -73,6 +74,7 @@ class TasksRelationManager extends RelationManager
return $table
->reorderable('sequence_id')
->defaultSort('sequence_id')
->reorderRecordsTriggerAction(fn (Action $action, bool $isReordering) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->columns([
TextColumn::make('action')
->label(trans('server/schedule.tasks.actions.title'))

View File

@@ -97,8 +97,7 @@ class ScheduleResource extends Resource
->formatStateUsing(fn (?Schedule $schedule) => $schedule?->status->value ?? 'new')
->options(fn (?Schedule $schedule) => [$schedule?->status->value ?? 'new' => $schedule?->status->getLabel() ?? 'New'])
->visibleOn('view'),
Section::make('Cron')
->label(trans('server/schedule.cron'))
Section::make(trans('server/schedule.cron'))
->description(function (Get $get) {
try {
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(user()->timezone ?? 'UTC');
@@ -110,22 +109,22 @@ class ScheduleResource extends Resource
})
->schema([
Actions::make([
CronPresetAction::make('hourly')
CronPresetAction::make('exclude_hourly')
->label(trans('server/schedule.time.hourly'))
->cron('0', '*', '*', '*', '*'),
CronPresetAction::make('daily')
CronPresetAction::make('exclude_daily')
->label(trans('server/schedule.time.daily'))
->cron('0', '0', '*', '*', '*'),
CronPresetAction::make('weekly_monday')
CronPresetAction::make('exclude_weekly_monday')
->label(trans('server/schedule.time.weekly_mon'))
->cron('0', '0', '*', '*', '1'),
CronPresetAction::make('weekly_sunday')
CronPresetAction::make('exclude_weekly_sunday')
->label(trans('server/schedule.time.weekly_sun'))
->cron('0', '0', '*', '*', '0'),
CronPresetAction::make('monthly')
CronPresetAction::make('exclude_monthly')
->label(trans('server/schedule.time.monthly'))
->cron('0', '0', '1', '*', '*'),
CronPresetAction::make('every_x_minutes')
CronPresetAction::make('exclude_every_x_minutes')
->label(trans('server/schedule.time.every_min'))
->color(fn (Get $get) => str($get('cron_minute'))->startsWith('*/')
&& $get('cron_hour') == '*'
@@ -148,7 +147,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_hours')
CronPresetAction::make('exclude_every_x_hours')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& str($get('cron_hour'))->startsWith('*/')
&& $get('cron_day_of_month') == '*'
@@ -170,7 +169,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_days')
CronPresetAction::make('exclude_every_x_days')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& str($get('cron_day_of_month'))->startsWith('*/')
@@ -192,7 +191,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_months')
CronPresetAction::make('exclude_every_x_months')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '1'
@@ -214,7 +213,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*/' . $data['x']);
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_day_of_week')
CronPresetAction::make('exclude_every_x_day_of_week')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '*'

View File

@@ -167,7 +167,7 @@ class SubuserResource extends Resource
])
->formatStateUsing(fn (Subuser $subuser) => $subuser->user->email),
Actions::make([
Action::make('assignAll')
Action::make('exclude_assignAll')
->label(trans('server/user.assign_all'))
->action(function (Set $set) use ($permissionsArray) {
$permissions = $permissionsArray;
@@ -244,7 +244,7 @@ class SubuserResource extends Resource
])
->required(),
Actions::make([
Action::make('assignAll')
Action::make('exclude_assignAll')
->label(trans('server/user.assign_all'))
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;

View File

@@ -77,7 +77,7 @@ class FileController extends ClientApiController
->property('file', $request->get('file'))
->log();
return new Response(convert_to_utf8($response), Response::HTTP_OK, ['Content-Type' => 'text/plain; charset=utf-8']);
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
}
/**

View File

@@ -10,6 +10,7 @@ use Filament\Notifications\Concerns\HasStatus;
use Filament\Notifications\Concerns\HasTitle;
use Filament\Support\Components\ViewComponent;
use Illuminate\Contracts\Support\Arrayable;
use Livewire\Livewire;
final class AlertBanner extends ViewComponent implements Arrayable
{
@@ -83,7 +84,13 @@ final class AlertBanner extends ViewComponent implements Arrayable
public function send(): AlertBanner
{
session()->push('alert-banners', $this->toArray());
$data = $this->toArray();
if (Livewire::isLivewireRequest()) {
$data['from_livewire'] = true;
}
session()->push('alert-banners', $data);
return $this;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Livewire;
use Filament\Notifications\Collection;
class AlertBannerCollection extends Collection
{
public static function fromLivewire($value): static
{
return (new static($value))->transform(
fn (array $alertBanner): AlertBanner => AlertBanner::fromArray($alertBanner),
);
}
}

View File

@@ -2,25 +2,35 @@
namespace App\Livewire;
use Filament\Notifications\Collection;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Component;
class AlertBannerContainer extends Component
{
public Collection $alertBanners;
public AlertBannerCollection $alertBanners;
public function mount(): void
{
$this->alertBanners = new Collection();
$this->pullFromSession();
$this->alertBanners = new AlertBannerCollection();
foreach (session()->pull('alert-banners', []) as $alertBanner) {
// Alerts created during Livewire requests should have been consumed by the event handler on the same page.
if (!empty($alertBanner['from_livewire'])) {
// If they weren't, then discard them instead of showing on the wrong page.
continue;
}
$alertBanner = AlertBanner::fromArray($alertBanner);
$this->alertBanners->put($alertBanner->getId(), $alertBanner);
}
}
#[On('alertBannerSent')]
public function pullFromSession(): void
{
foreach (session()->pull('alert-banners', []) as $alertBanner) {
unset($alertBanner['from_livewire']);
$alertBanner = AlertBanner::fromArray($alertBanner);
$this->alertBanners->put($alertBanner->getId(), $alertBanner);
}

View File

@@ -109,13 +109,11 @@ class Plugin extends Model implements HasPluginSettings
continue;
}
$plugin = Str::lower($plugin);
try {
$data = File::json($path, JSON_THROW_ON_ERROR);
$data['id'] = Str::lower($data['id']);
if ($data['id'] !== $plugin) {
if ($data['id'] !== Str::lower($plugin)) {
throw new PluginIdMismatchException("Plugin id mismatch for folder name ($plugin) and id in plugin.json ({$data['id']})!");
}
@@ -161,7 +159,7 @@ class Plugin extends Model implements HasPluginSettings
if (!$exception instanceof JsonException) {
$plugins[] = [
'id' => $data['id'] ?? Str::uuid(),
'id' => $exception instanceof PluginIdMismatchException ? $plugin : ($data['id'] ?? Str::uuid()),
'name' => $data['name'] ?? Str::headline($plugin),
'author' => $data['author'] ?? 'Unknown',
'version' => $data['version'] ?? '0.0.0',

View File

@@ -34,7 +34,7 @@ use Illuminate\Support\ServiceProvider;
use Livewire\Component;
use Livewire\Livewire;
use function Livewire\on;
use function Livewire\before;
use function Livewire\store;
class FilamentServiceProvider extends ServiceProvider
@@ -74,7 +74,7 @@ class FilamentServiceProvider extends ServiceProvider
fn () => Blade::render("@vite(['resources/js/app.js'])"),
);
on('dehydrate', function (Component $component) {
before('dehydrate', function (Component $component) {
if (!Livewire::isLivewireRequest()) {
return;
}

View File

@@ -39,7 +39,7 @@ class PluginService
/** @var ClassLoader $classLoader */
$classLoader = File::getRequire(base_path('vendor/autoload.php'));
$plugins = Plugin::query()->orderBy('load_order')->get();
$plugins = Plugin::orderBy('load_order')->get();
foreach ($plugins as $plugin) {
try {
// Filter out plugins that are not compatible with the current panel version
@@ -138,7 +138,7 @@ class PluginService
return;
}
$plugins = Plugin::query()->orderBy('load_order')->get();
$plugins = Plugin::orderBy('load_order')->get();
foreach ($plugins as $plugin) {
try {
if (!$plugin->shouldLoad($panel->getId())) {
@@ -172,7 +172,7 @@ class PluginService
{
$newPackages ??= [];
$plugins = Plugin::query()->orderBy('load_order')->get();
$plugins = Plugin::orderBy('load_order')->get();
foreach ($plugins as $plugin) {
if (!$plugin->composer_packages) {
continue;
@@ -434,7 +434,7 @@ class PluginService
/** @param array<string, mixed> $data */
private function setMetaData(string|Plugin $plugin, array $data): void
{
$path = plugin_path($plugin instanceof Plugin ? $plugin->id : $plugin, 'plugin.json');
$path = plugin_path($plugin->id, 'plugin.json');
if (File::exists($path)) {
$pluginData = File::json($path, JSON_THROW_ON_ERROR);
@@ -443,7 +443,6 @@ class PluginService
File::put($path, json_encode($pluginData, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$plugin = $plugin instanceof Plugin ? $plugin : Plugin::findOrFail($plugin);
$plugin->update($metaData);
}
}
@@ -464,6 +463,8 @@ class PluginService
public function updateLoadOrder(array $order): void
{
foreach ($order as $i => $plugin) {
$plugin = Plugin::firstOrFail(str($plugin)->lower()->toString());
$this->setMetaData($plugin, [
'load_order' => $i,
]);
@@ -472,7 +473,7 @@ class PluginService
public function hasThemePluginEnabled(): bool
{
$plugins = Plugin::query()->orderBy('load_order')->get();
$plugins = Plugin::orderBy('load_order')->get();
foreach ($plugins as $plugin) {
if ($plugin->isTheme() && $plugin->status === PluginStatus::Enabled) {
return true;
@@ -487,7 +488,7 @@ class PluginService
{
$languages = [];
$plugins = Plugin::query()->orderBy('load_order')->get();
$plugins = Plugin::orderBy('load_order')->get();
foreach ($plugins as $plugin) {
if ($plugin->status !== PluginStatus::Enabled || !$plugin->isLanguage()) {
continue;
@@ -504,7 +505,7 @@ class PluginService
return config('panel.plugin.dev_mode', false);
}
private function handlePluginException(string|Plugin $plugin, Exception $exception): void
private function handlePluginException(Plugin $plugin, Exception $exception): void
{
if ($this->isDevModeActive()) {
throw ($exception);

View File

@@ -130,27 +130,6 @@ if (!function_exists('encode_path')) {
}
}
if (!function_exists('convert_to_utf8')) {
/**
* Convert a string to UTF-8 from an unknown encoding
*/
function convert_to_utf8(string $contents): string
{
// Valid UTF-8 passes through unchanged
if (mb_check_encoding($contents, 'UTF-8')) {
return $contents;
}
// Only detect UTF-16 by BOM instead of mb_check_encoding('UTF-16') which can cause false positives
if (str_starts_with($contents, "\xFF\xFE") || str_starts_with($contents, "\xFE\xFF")) {
return mb_convert_encoding($contents, 'UTF-8', 'UTF-16');
}
// ISO-8859-1 serves as a universal fallback since any byte sequence is valid in it
return mb_convert_encoding($contents, 'UTF-8', 'ISO-8859-1');
}
}
if (!function_exists('user')) {
function user(): ?App\Models\User
{

View File

@@ -13,11 +13,11 @@
"calebporzio/sushi": "^2.5",
"dedoc/scramble": "^0.13",
"filament/filament": "^4.5",
"gboquizosanchez/filament-log-viewer": "^2.1",
"gboquizosanchez/filament-log-viewer": "^2.2",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.51",
"laravel/framework": "^12.52",
"laravel/helpers": "^1.8",
"laravel/sanctum": "^4.2",
"laravel/sanctum": "^4.3",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
@@ -34,7 +34,7 @@
"socialiteproviders/steam": "^4.3",
"spatie/laravel-data": "^4.19",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-health": "^1.34",
"spatie/laravel-health": "^1.37",
"spatie/laravel-permission": "^6.24",
"spatie/laravel-query-builder": "^6.4",
"spatie/temporary-directory": "^2.3",
@@ -55,8 +55,7 @@
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7",
"pestphp/pest-plugin-faker": "^3.0",
"pestphp/pest-plugin-livewire": "^3.0",
"spatie/laravel-ignition": "^2.9"
"pestphp/pest-plugin-livewire": "^3.0"
},
"autoload": {
"files": [

543
composer.lock generated
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": "784a5e04d3c28e0dba289d45a239075e",
"content-hash": "f6d587063490fa0aeb46b8f05dd30a22",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -128,16 +128,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.369.31",
"version": "3.369.36",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "c7bf53dfb09bea3ebfd19b89213625aa134dcc71"
"reference": "2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c7bf53dfb09bea3ebfd19b89213625aa134dcc71",
"reference": "c7bf53dfb09bea3ebfd19b89213625aa134dcc71",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0",
"reference": "2a69e7df5e03be9e08f9f73fb6a8cc9dd63b59c0",
"shasum": ""
},
"require": {
@@ -219,9 +219,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.31"
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.36"
},
"time": "2026-02-10T19:13:30+00:00"
"time": "2026-02-17T19:45:01+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -822,16 +822,16 @@
},
{
"name": "dedoc/scramble",
"version": "v0.13.12",
"version": "v0.13.14",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "1788ab68ae51ae2fce34e16add1387ee1ac5d88b"
"reference": "8f0c1bba364e4916f3f2ff23b7f4ca002e586b75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/1788ab68ae51ae2fce34e16add1387ee1ac5d88b",
"reference": "1788ab68ae51ae2fce34e16add1387ee1ac5d88b",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/8f0c1bba364e4916f3f2ff23b7f4ca002e586b75",
"reference": "8f0c1bba364e4916f3f2ff23b7f4ca002e586b75",
"shasum": ""
},
"require": {
@@ -890,7 +890,7 @@
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.13.12"
"source": "https://github.com/dedoc/scramble/tree/v0.13.14"
},
"funding": [
{
@@ -898,7 +898,7 @@
"type": "github"
}
],
"time": "2026-02-05T07:47:09+00:00"
"time": "2026-02-15T13:14:31+00:00"
},
{
"name": "dflydev/dot-access-data",
@@ -2544,16 +2544,16 @@
},
{
"name": "laravel/framework",
"version": "v12.51.0",
"version": "v12.52.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "ce4de3feb211e47c4f959d309ccf8a2733b1bc16"
"reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/ce4de3feb211e47c4f959d309ccf8a2733b1bc16",
"reference": "ce4de3feb211e47c4f959d309ccf8a2733b1bc16",
"url": "https://api.github.com/repos/laravel/framework/zipball/d5511fa74f4608dbb99864198b1954042aa8d5a7",
"reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7",
"shasum": ""
},
"require": {
@@ -2762,7 +2762,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-02-10T18:20:19+00:00"
"time": "2026-02-17T17:07:04+00:00"
},
{
"name": "laravel/helpers",
@@ -4877,16 +4877,16 @@
},
{
"name": "nette/utils",
"version": "v4.1.2",
"version": "v4.1.3",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5"
"reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
"reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
"url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe",
"reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe",
"shasum": ""
},
"require": {
@@ -4898,8 +4898,10 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
"phpstan/phpstan": "^2.0@stable",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -4960,9 +4962,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.1.2"
"source": "https://github.com/nette/utils/tree/v4.1.3"
},
"time": "2026-02-03T17:21:09+00:00"
"time": "2026-02-13T03:05:33+00:00"
},
{
"name": "nikic/php-parser",
@@ -5024,31 +5026,31 @@
},
{
"name": "nunomaduro/termwind",
"version": "v2.3.3",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
"reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017"
"reference": "712a31b768f5daea284c2169a7d227031001b9a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017",
"reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8",
"reference": "712a31b768f5daea284c2169a7d227031001b9a8",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.2",
"symfony/console": "^7.3.6"
"symfony/console": "^7.4.4 || ^8.0.4"
},
"require-dev": {
"illuminate/console": "^11.46.1",
"laravel/pint": "^1.25.1",
"illuminate/console": "^11.47.0",
"laravel/pint": "^1.27.1",
"mockery/mockery": "^1.6.12",
"pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3",
"pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2",
"phpstan/phpstan": "^1.12.32",
"phpstan/phpstan-strict-rules": "^1.6.2",
"symfony/var-dumper": "^7.3.5",
"symfony/var-dumper": "^7.3.5 || ^8.0.4",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
@@ -5080,7 +5082,7 @@
"email": "enunomaduro@gmail.com"
}
],
"description": "Its like Tailwind CSS, but for the console.",
"description": "It's like Tailwind CSS, but for the console.",
"keywords": [
"cli",
"console",
@@ -5091,7 +5093,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.3"
"source": "https://github.com/nunomaduro/termwind/tree/v2.4.0"
},
"funding": [
{
@@ -5107,7 +5109,7 @@
"type": "github"
}
],
"time": "2025-11-20T02:34:59+00:00"
"time": "2026-02-16T23:10:27+00:00"
},
{
"name": "openspout/openspout",
@@ -6526,16 +6528,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.19",
"version": "v0.12.20",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee"
"reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/a4f766e5c5b6773d8399711019bb7d90875a50ee",
"reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373",
"reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373",
"shasum": ""
},
"require": {
@@ -6599,9 +6601,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.19"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.20"
},
"time": "2026-01-30T17:33:13+00:00"
"time": "2026-02-11T15:05:28+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -7657,16 +7659,16 @@
},
{
"name": "spatie/laravel-health",
"version": "1.36.0",
"version": "1.37.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-health.git",
"reference": "e2a84e886cb38ab0a53f136e4554c64e0480e634"
"reference": "2d3a68ae2f855d3997a85deb819898a4b7720d49"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/e2a84e886cb38ab0a53f136e4554c64e0480e634",
"reference": "e2a84e886cb38ab0a53f136e4554c64e0480e634",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/2d3a68ae2f855d3997a85deb819898a4b7720d49",
"reference": "2d3a68ae2f855d3997a85deb819898a4b7720d49",
"shasum": ""
},
"require": {
@@ -7738,7 +7740,7 @@
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-health/tree/1.36.0"
"source": "https://github.com/spatie/laravel-health/tree/1.37.0"
},
"funding": [
{
@@ -7746,7 +7748,7 @@
"type": "github"
}
],
"time": "2026-02-09T15:38:27+00:00"
"time": "2026-02-12T16:31:50+00:00"
},
{
"name": "spatie/laravel-package-tools",
@@ -12744,39 +12746,36 @@
},
{
"name": "nunomaduro/collision",
"version": "v8.8.3",
"version": "v8.9.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
"reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4"
"reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4",
"reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
"reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935",
"shasum": ""
},
"require": {
"filp/whoops": "^2.18.1",
"nunomaduro/termwind": "^2.3.1",
"filp/whoops": "^2.18.4",
"nunomaduro/termwind": "^2.4.0",
"php": "^8.2.0",
"symfony/console": "^7.3.0"
"symfony/console": "^7.4.4 || ^8.0.4"
},
"conflict": {
"laravel/framework": "<11.44.2 || >=13.0.0",
"phpunit/phpunit": "<11.5.15 || >=13.0.0"
"laravel/framework": "<11.48.0 || >=14.0.0",
"phpunit/phpunit": "<11.5.50 || >=14.0.0"
},
"require-dev": {
"brianium/paratest": "^7.8.3",
"larastan/larastan": "^3.4.2",
"laravel/framework": "^11.44.2 || ^12.18",
"laravel/pint": "^1.22.1",
"laravel/sail": "^1.43.1",
"laravel/sanctum": "^4.1.1",
"laravel/tinker": "^2.10.1",
"orchestra/testbench-core": "^9.12.0 || ^10.4",
"pestphp/pest": "^3.8.2 || ^4.0.0",
"sebastian/environment": "^7.2.1 || ^8.0"
"brianium/paratest": "^7.8.5",
"larastan/larastan": "^3.9.2",
"laravel/framework": "^11.48.0 || ^12.52.0",
"laravel/pint": "^1.27.1",
"orchestra/testbench-core": "^9.12.0 || ^10.9.0",
"pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0",
"sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0"
},
"type": "library",
"extra": {
@@ -12839,7 +12838,7 @@
"type": "patreon"
}
],
"time": "2025-11-20T02:55:25+00:00"
"time": "2026-02-17T17:33:08+00:00"
},
{
"name": "pestphp/pest",
@@ -13416,11 +13415,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.38",
"version": "2.1.39",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629",
"reference": "dfaf1f530e1663aa167bc3e52197adb221582629",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224",
"reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224",
"shasum": ""
},
"require": {
@@ -13465,7 +13464,7 @@
"type": "github"
}
],
"time": "2026-01-30T17:12:46+00:00"
"time": "2026-02-11T14:48:56+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -14909,388 +14908,6 @@
],
"time": "2024-10-09T05:16:32+00:00"
},
{
"name": "spatie/backtrace",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
"reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/8c0f16a59ae35ec8c62d85c3c17585158f430110",
"reference": "8c0f16a59ae35ec8c62d85c3c17585158f430110",
"shasum": ""
},
"require": {
"php": "^7.3 || ^8.0"
},
"require-dev": {
"ext-json": "*",
"laravel/serializable-closure": "^1.3 || ^2.0",
"phpunit/phpunit": "^9.3 || ^11.4.3",
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
"symfony/var-dumper": "^5.1 || ^6.0 || ^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Backtrace\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van de Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "A better backtrace",
"homepage": "https://github.com/spatie/backtrace",
"keywords": [
"Backtrace",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/backtrace/issues",
"source": "https://github.com/spatie/backtrace/tree/1.8.1"
},
"funding": [
{
"url": "https://github.com/sponsors/spatie",
"type": "github"
},
{
"url": "https://spatie.be/open-source/support-us",
"type": "other"
}
],
"time": "2025-08-26T08:22:30+00:00"
},
{
"name": "spatie/error-solutions",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/error-solutions.git",
"reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
"reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"illuminate/broadcasting": "^10.0|^11.0|^12.0",
"illuminate/cache": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"livewire/livewire": "^2.11|^3.5.20",
"openai-php/client": "^0.10.1",
"orchestra/testbench": "8.22.3|^9.0|^10.0",
"pestphp/pest": "^2.20|^3.0",
"phpstan/phpstan": "^2.1",
"psr/simple-cache": "^3.0",
"psr/simple-cache-implementation": "^3.0",
"spatie/ray": "^1.28",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"simple-cache-implementation": "To cache solutions from OpenAI"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Ignition\\": "legacy/ignition",
"Spatie\\ErrorSolutions\\": "src",
"Spatie\\LaravelIgnition\\": "legacy/laravel-ignition"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"role": "Developer"
}
],
"description": "This is my package error-solutions",
"homepage": "https://github.com/spatie/error-solutions",
"keywords": [
"error-solutions",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/error-solutions/issues",
"source": "https://github.com/spatie/error-solutions/tree/1.1.3"
},
"funding": [
{
"url": "https://github.com/Spatie",
"type": "github"
}
],
"time": "2025-02-14T12:29:50+00:00"
},
{
"name": "spatie/flare-client-php",
"version": "1.10.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
"reference": "bf1716eb98bd689451b071548ae9e70738dce62f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f",
"reference": "bf1716eb98bd689451b071548ae9e70738dce62f",
"shasum": ""
},
"require": {
"illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"spatie/backtrace": "^1.6.1",
"symfony/http-foundation": "^5.2|^6.0|^7.0",
"symfony/mime": "^5.2|^6.0|^7.0",
"symfony/process": "^5.2|^6.0|^7.0",
"symfony/var-dumper": "^5.2|^6.0|^7.0"
},
"require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"pestphp/pest": "^1.20|^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"spatie/pest-plugin-snapshots": "^1.0|^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.3.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\FlareClient\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Send PHP errors to Flare",
"homepage": "https://github.com/spatie/flare-client-php",
"keywords": [
"exception",
"flare",
"reporting",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
"source": "https://github.com/spatie/flare-client-php/tree/1.10.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-14T13:42:06+00:00"
},
{
"name": "spatie/ignition",
"version": "1.15.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/ignition.git",
"reference": "31f314153020aee5af3537e507fef892ffbf8c85"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85",
"reference": "31f314153020aee5af3537e507fef892ffbf8c85",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^8.0",
"spatie/error-solutions": "^1.0",
"spatie/flare-client-php": "^1.7",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0"
},
"require-dev": {
"illuminate/cache": "^9.52|^10.0|^11.0|^12.0",
"mockery/mockery": "^1.4",
"pestphp/pest": "^1.20|^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"psr/simple-cache-implementation": "*",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"simple-cache-implementation": "To cache solutions from OpenAI"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.5.x-dev"
}
},
"autoload": {
"psr-4": {
"Spatie\\Ignition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Spatie",
"email": "info@spatie.be",
"role": "Developer"
}
],
"description": "A beautiful error page for PHP applications.",
"homepage": "https://flareapp.io/ignition",
"keywords": [
"error",
"flare",
"laravel",
"page"
],
"support": {
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
"forum": "https://twitter.com/flareappio",
"issues": "https://github.com/spatie/ignition/issues",
"source": "https://github.com/spatie/ignition"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-21T14:31:39+00:00"
},
{
"name": "spatie/laravel-ignition",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2abefdcca6074a9155f90b4ccb3345af8889d5f5",
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"illuminate/support": "^11.0|^12.0",
"nesbot/carbon": "^2.72|^3.0",
"php": "^8.2",
"spatie/ignition": "^1.15.1",
"symfony/console": "^7.4|^8.0",
"symfony/var-dumper": "^7.4|^8.0"
},
"require-dev": {
"livewire/livewire": "^3.7.0|^4.0",
"mockery/mockery": "^1.6.12",
"openai-php/client": "^0.10.3",
"orchestra/testbench": "^v9.16.0|^10.6",
"pestphp/pest": "^3.7|^4.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.8",
"vlucas/phpdotenv": "^5.6.2"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"psr/simple-cache-implementation": "Needed to cache solutions from OpenAI"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Flare": "Spatie\\LaravelIgnition\\Facades\\Flare"
},
"providers": [
"Spatie\\LaravelIgnition\\IgnitionServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\LaravelIgnition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Spatie",
"email": "info@spatie.be",
"role": "Developer"
}
],
"description": "A beautiful error page for Laravel applications.",
"homepage": "https://flareapp.io/ignition",
"keywords": [
"error",
"flare",
"laravel",
"page"
],
"support": {
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
"forum": "https://twitter.com/flareappio",
"issues": "https://github.com/spatie/laravel-ignition/issues",
"source": "https://github.com/spatie/laravel-ignition"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-01-20T13:16:11+00:00"
},
{
"name": "staabm/side-effects-detector",
"version": "1.0.5",
@@ -15345,23 +14962,23 @@
},
{
"name": "ta-tikoma/phpunit-architecture-test",
"version": "0.8.6",
"version": "0.8.7",
"source": {
"type": "git",
"url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
"reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e"
"reference": "1248f3f506ca9641d4f68cebcd538fa489754db8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8",
"reference": "1248f3f506ca9641d4f68cebcd538fa489754db8",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18.0 || ^5.0.0",
"php": "^8.1.0",
"phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0",
"phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
"phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0"
},
"require-dev": {
@@ -15398,9 +15015,9 @@
],
"support": {
"issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
"source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6"
"source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7"
},
"time": "2026-01-30T07:16:00+00:00"
"time": "2026-02-17T17:25:14+00:00"
},
{
"name": "theseer/tokenizer",
@@ -15470,5 +15087,5 @@
"platform-overrides": {
"php": "8.2"
},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}

View File

@@ -8,29 +8,35 @@ return new class extends Migration
{
$mappings = [
// Forge Minecraft
'ed072427-f209-4603-875c-f540c6dd5a65' => [
'new_uuid' => 'd6018085-eecc-42bf-bf8c-51ea45a69ace',
'd6018085-eecc-42bf-bf8c-51ea45a69ace' => [
'new_uuid' => 'ed072427-f209-4603-875c-f540c6dd5a65',
'new_update_url' => 'https://raw.githubusercontent.com/pelican-eggs/minecraft/refs/heads/main/java/forge/egg-forge-minecraft.yaml',
],
// Paper
'5da37ef6-58da-4169-90a6-e683e1721247' => [
'new_uuid' => '150956be-4164-4086-9057-631ae95505e9',
'150956be-4164-4086-9057-631ae95505e9' => [
'new_uuid' => '5da37ef6-58da-4169-90a6-e683e1721247',
'new_update_url' => 'https://raw.githubusercontent.com/pelican-eggs/minecraft/refs/heads/main/java/paper/egg-paper.yaml',
],
// Garrys Mod
'60ef81d4-30a2-4d98-ab64-f59c69e2f915' => [
'new_uuid' => 'c0b2f96a-f753-4d82-a73e-6e5be2bbadd5',
'c0b2f96a-f753-4d82-a73e-6e5be2bbadd5' => [
'new_uuid' => '60ef81d4-30a2-4d98-ab64-f59c69e2f915',
'new_update_url' => 'https://raw.githubusercontent.com/pelican-eggs/games-steamcmd/refs/heads/main/gmod/egg-garrys-mod.yaml',
],
];
foreach ($mappings as $oldUuid => $newData) {
DB::table('eggs')->where('uuid', $oldUuid)->update([
'uuid' => $newData['new_uuid'],
'update_url' => $newData['new_update_url'],
]);
if (DB::table('eggs')->where('uuid', $newData['new_uuid'])->exists()) {
DB::table('eggs')->where('uuid', $newData['new_uuid'])->update([
'update_url' => $newData['new_update_url'],
]);
} else {
DB::table('eggs')->where('uuid', $oldUuid)->update([
'uuid' => $newData['new_uuid'],
'update_url' => $newData['new_update_url'],
]);
}
}
}

View File

@@ -5,11 +5,11 @@
{$CADDY_STRICT_PROXIES}
}
admin off
{$PARSED_AUTO_HTTPS}
{$PARSED_LE_EMAIL}
{$CADDY_AUTO_HTTPS}
{$CADDY_LE_EMAIL}
}
{$PARSED_APP_URL} {
{$CADDY_APP_URL} {
root * /var/www/html/public
encode gzip

View File

@@ -1,34 +1,48 @@
#!/bin/ash -e
# shellcheck shell=dash
# check for .env file or symlink and generate app keys if missing
if [ -f /var/www/html/.env ]; then
echo "external vars exist."
if [ -f /pelican-data/.env ]; then
echo ".env vars exist."
# load specific env vars from .env used in the entrypoint and they are not already set
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do if ! (printenv | grep -q ${VAR}); then export $(grep ${VAR} .env | grep -ve "^#"); fi; done
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do
echo "checking for ${VAR}"
## skip if it looks like it might try to execute code
if (grep "${VAR}" .env | grep -qE "\$\(|=\`|\$#"); then echo "var in .env may be executable or a comment, skipping"; continue; fi
# if the variable is in .env then set it
if (grep -q "${VAR}" .env); then
echo "loading ${VAR} from .env"
export "$(grep "${VAR}" .env | sed 's/"//g')"
continue
fi
## variable wasn't loaded or in the env to set
echo "didn't find variable to set"
done
else
echo "external vars don't exist."
echo ".env vars don't exist."
# webroot .env is symlinked to this path
touch /pelican-data/.env
# manually generate a key because key generate --force fails
if [ -z ${APP_KEY} ]; then
echo -e "Generating key."
if [ -z "${APP_KEY}" ]; then
echo "No key set, Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo -e "Generated app key: $APP_KEY"
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "Generated app key written to .env file"
else
echo -e "APP_KEY exists in environment, using that."
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "APP_KEY exists in environment, using that."
echo "APP_KEY=$APP_KEY" > /pelican-data/.env
fi
# enable installer
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
echo "APP_INSTALLED=false" >> /pelican-data/.env
fi
# create directories for volumes
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /pelican-data/storage/icons /pelican-data/plugins /var/www/html/storage/logs/supervisord 2>/dev/null
# if the app is installed then we need to run migrations on start. New installs will run migrations when you run the installer.
if [ "${APP_INSTALLED}" == "true" ]; then
if [ "${APP_INSTALLED}" = "true" ]; then
#if the db is anything but sqlite wait until it's accepting connections
if [ "${DB_CONNECTION}" != "sqlite" ]; then
# check for DB up before starting the panel
@@ -39,36 +53,44 @@ if [ "${APP_INSTALLED}" == "true" ]; then
# wait for 1 seconds before check again
sleep 1
done
else
echo "using sqlite database"
fi
# run migration
php artisan migrate --force
fi
echo -e "Optimizing Filament"
echo "Optimizing Filament"
php artisan filament:optimize
# default to caddy not starting
export SUPERVISORD_CADDY=false
export PARSED_APP_URL=${APP_URL}
export CADDY_APP_URL="${APP_URL}"
# checking if app url is using https
if echo "${APP_URL}" | grep -qE '^https://'; then
# checking if app url is https
if (echo "${APP_URL}" | grep -qE '^https://'); then
# check lets encrypt email was set without a proxy
if [ -z "${LE_EMAIL}" ] && [ "${BEHIND_PROXY}" != "true" ]; then
echo "when app url is https a lets encrypt email must be set when not behind a proxy"
exit 1
fi
echo "https domain found setting email var"
export PARSED_LE_EMAIL="email ${LE_EMAIL}"
export CADDY_LE_EMAIL="email ${LE_EMAIL}"
fi
# when running behind a proxy
if [ "${BEHIND_PROXY}" == "true" ]; then
if [ "${BEHIND_PROXY}" = "true" ]; then
echo "running behind proxy"
echo "listening on port 80 internally"
export PARSED_LE_EMAIL=""
export PARSED_APP_URL=":80"
export PARSED_AUTO_HTTPS="auto_https off"
export ASSET_URL=${APP_URL}
export CADDY_LE_EMAIL=""
export CADDY_APP_URL=":80"
export CADDY_AUTO_HTTPS="auto_https off"
export ASSET_URL="${APP_URL}"
fi
# disable caddy if SKIP_CADDY is set
if [ "${SKIP_CADDY:-}" == "true" ]; then
if [ "${SKIP_CADDY:-}" = "true" ]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
@@ -76,8 +98,9 @@ else
export SUPERVISORD_CADDY=true
# handle trusted proxies for caddy when variable has data
if [ ! -z ${TRUSTED_PROXIES} ]; then
export CADDY_TRUSTED_PROXIES=$(echo "trusted_proxies static ${TRUSTED_PROXIES}" | sed 's/,/ /g')
if [ -n "${TRUSTED_PROXIES:-}" ]; then
FORMATTED_PROXIES=$(echo "trusted_proxies static ${TRUSTED_PROXIES}" | sed 's/,/ /g')
export CADDY_TRUSTED_PROXIES="${FORMATTED_PROXIES}"
export CADDY_STRICT_PROXIES="trusted_proxies_strict"
fi
fi

View File

@@ -31,6 +31,7 @@ return [
'no_local_ip' => 'Local IP Addresses are not allowed',
'unsupported_format' => 'Unsupported Format. Supported Formats: :formats',
'invalid_url' => 'The provided URL is invalid',
'unknown_extension' => 'Unknown image extension',
'image_deleted' => 'Image Deleted',
'no_image' => 'No Image Provided',
'image_updated' => 'Image Updated',

View File

@@ -69,4 +69,5 @@ return [
'no_oauth' => 'No Accounts Linked',
'no_api_keys' => 'No API Keys',
'no_ssh_keys' => 'No SSH Keys',
'activity_info' => 'Showing last 50 activity logs',
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,120 +0,0 @@
<?php
namespace App\Tests\Unit\Helpers;
use App\Tests\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
class ConvertToUtf8Test extends TestCase
{
#[DataProvider('helperDataProvider')]
public function test_helper(string $input, string $expected): void
{
$result = convert_to_utf8($input);
$this->assertSame($expected, $result);
}
/**
* Every output must be valid UTF-8, regardless of input encoding.
*/
#[DataProvider('helperDataProvider')]
public function test_output_is_valid_utf8(string $input): void
{
$result = convert_to_utf8($input);
$this->assertTrue(mb_check_encoding($result, 'UTF-8'), 'Output is not valid UTF-8: ' . bin2hex($result));
}
/**
* Running convert_to_utf8 twice must produce the same result as once.
*/
#[DataProvider('helperDataProvider')]
public function test_idempotent(string $input): void
{
$once = convert_to_utf8($input);
$twice = convert_to_utf8($once);
$this->assertSame($once, $twice, 'Double conversion changed the output (double-encoding bug): ' . bin2hex($once) . ' → ' . bin2hex($twice));
}
public static function helperDataProvider(): array
{
return [
// UTF-8 passthrough - must never be re-encoded
'ascii string' => ['hello world', 'hello world'],
'empty string' => ['', ''],
'utf8 accented cafe' => ["caf\xC3\xA9", "caf\xC3\xA9"],
'utf8 emoji' => ["\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80"],
'utf8 cjk characters' => ["\xE4\xB8\xAD\xE6\x96\x87", "\xE4\xB8\xAD\xE6\x96\x87"],
'utf8 cyrillic' => ["\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"], // Привет
'utf8 bom preserved' => ["\xEF\xBB\xBFhello", "\xEF\xBB\xBFhello"],
'utf8 null byte' => ["a\x00b", "a\x00b"],
// Issue #2187 - small caps were double-encoded breaking Monaco display
'utf8 small caps phrase (issue #2187)' => [
"\xE1\xB4\x9B\xCA\x9C\xC9\xAA\xEA\x9C\xB1 \xC9\xAA\xEA\x9C\xB1 \xE1\xB4\x80\xC9\xB4 \xE1\xB4\x87x\xE1\xB4\x80\xE1\xB4\x8D\xE1\xB4\x98\xCA\x9F\xE1\xB4\x87",
"\xE1\xB4\x9B\xCA\x9C\xC9\xAA\xEA\x9C\xB1 \xC9\xAA\xEA\x9C\xB1 \xE1\xB4\x80\xC9\xB4 \xE1\xB4\x87x\xE1\xB4\x80\xE1\xB4\x8D\xE1\xB4\x98\xCA\x9F\xE1\xB4\x87",
],
// Minecraft § color codes - extremely common in game server files
'utf8 minecraft section sign' => ["\xC2\xA7aGreen \xC2\xA7cRed", "\xC2\xA7aGreen \xC2\xA7cRed"],
'iso-8859-1 minecraft section sign' => ["\xA7aGreen", "\xC2\xA7aGreen"],
// PR #2199 double-encoding regression - UTF-8 é (\xC3\xA9) must NOT become \xC3\x83\xC2\xA9
'no double encoding of utf8 e-acute' => ["\xC3\xA9", "\xC3\xA9"],
'no double encoding of utf8 multi-byte' => ["\xC3\xBC\xC3\xA4\xC3\xB6", "\xC3\xBC\xC3\xA4\xC3\xB6"], // üäö
// Issue #1606 - ISO-8859-1 files couldn't be edited
'iso-8859-1 cafe (issue #1606)' => ["caf\xE9", "caf\xC3\xA9"],
'iso-8859-1 german umlauts' => ["\xFC\xE4\xF6", "\xC3\xBC\xC3\xA4\xC3\xB6"], // üäö
'iso-8859-1 full range latin' => ["\xE0\xE8\xEC\xF2\xF9", "\xC3\xA0\xC3\xA8\xC3\xAC\xC3\xB2\xC3\xB9"], // àèìòù
'iso-8859-1 single high byte 0xFF' => ["\xFF", "\xC3\xBF"], // ÿ
'iso-8859-1 copyright symbol' => ["\xA9 2026", "\xC2\xA9 2026"], // ©
// PR #1896 - UTF-16LE files from Windows
'utf16le with bom' => ["\xFF\xFEh\x00i\x00", 'hi'],
'utf16be with bom' => ["\xFE\xFF\x00h\x00i", 'hi'],
'utf16le with bom and accents' => ["\xFF\xFE" . "c\x00a\x00f\x00\xE9\x00", "caf\xC3\xA9"],
'utf16le with bom multiline' => ["\xFF\xFE" . "l\x00i\x00n\x00e\x001\x00\x0A\x00l\x00i\x00n\x00e\x002\x00", "line1\nline2"],
'utf16le bom only no content' => ["\xFF\xFE", ''],
'utf16be bom only no content' => ["\xFE\xFF", ''],
// PR #1991 - Windows-1252 smart quotes fall through to ISO-8859-1 fallback.
// Bytes 0x80-0x9F are C1 control chars in ISO-8859-1 but printable in Windows-1252.
// The result is valid UTF-8 (C1 control codepoints), not the "smart" characters,
// but this is acceptable... it doesn't crash and the output is valid UTF-8.
'windows-1252 smart quotes via iso-8859-1 fallback' => [
"\x93Hello\x94",
"\xC2\x93Hello\xC2\x94",
],
'windows-1252 em dash via iso-8859-1 fallback' => [
"word\x97word",
"word\xC2\x97word",
],
// Truncated UTF-8: max_edit_size can cut a file mid-character.
// \xC3 starts a 2-byte sequence but has no continuation byte → invalid UTF-8 → ISO-8859-1 fallback
'truncated utf8 2-byte sequence' => ["hello\xC3", 'helloÃ'],
'truncated utf8 3-byte sequence' => ["hello\xE2\x80", "helloâ\xC2\x80"],
'truncated utf8 4-byte sequence' => ["hello\xF0\x9F\x98", "helloð\xC2\x9F\xC2\x98"],
// Lone continuation bytes - invalid UTF-8, falls to ISO-8859-1
'lone continuation byte 0x80' => ["\x80", "\xC2\x80"],
'lone continuation bytes' => ["\x80\x81\x82", "\xC2\x80\xC2\x81\xC2\x82"],
// UTF-16 without BOM - by design falls to ISO-8859-1, not detected as UTF-16.
// This produces garbled output, but it's safe (valid UTF-8) and avoids false positives.
'utf16le without bom falls to iso-8859-1' => ["h\x00i\x00", "h\x00i\x00"],
// Game server config file content (real-world)
'ascii config with equals and brackets' => [
"[server]\nname=My Server\nport=25565",
"[server]\nname=My Server\nport=25565",
],
'yaml config with utf8' => [
"motd: \"Willkommen \xC3\xBC\"\nmax-players: 20",
"motd: \"Willkommen \xC3\xBC\"\nmax-players: 20",
],
'windows line endings' => ["line1\r\nline2\r\n", "line1\r\nline2\r\n"],
'tab separated values' => ["key\tvalue\nfoo\tbar", "key\tvalue\nfoo\tbar"],
];
}
}