mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 09:20:54 -04:00
refactor(livewire): make data-sync helpers private (#11578)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2018e7f329
commit
7c3b05e2d2
@@ -43,7 +43,7 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -85,11 +85,10 @@ class Discord extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->discord_enabled = $this->discordEnabled;
|
||||
$this->settings->discord_webhook_url = $this->discordWebhookUrl;
|
||||
|
||||
@@ -174,6 +173,7 @@ class Discord extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -184,6 +184,7 @@ class Discord extends Component
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -193,6 +194,8 @@ class Discord extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
refreshSession();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
|
||||
@@ -132,12 +132,11 @@ class Email extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->validate(['smtpEhloDomain' => ['nullable', 'string', new ValidHostname]]);
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
$this->settings->smtp_from_name = $this->smtpFromName;
|
||||
@@ -224,6 +223,8 @@ class Email extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Email notifications settings updated.');
|
||||
}
|
||||
|
||||
@@ -89,11 +89,10 @@ class Pushover extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->pushover_enabled = $this->pushoverEnabled;
|
||||
$this->settings->pushover_user_key = $this->pushoverUserKey;
|
||||
$this->settings->pushover_api_token = $this->pushoverApiToken;
|
||||
@@ -167,6 +166,7 @@ class Pushover extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -179,6 +179,7 @@ class Pushover extends Component
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -188,6 +189,8 @@ class Pushover extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
refreshSession();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
|
||||
@@ -87,11 +87,10 @@ class Slack extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->slack_enabled = $this->slackEnabled;
|
||||
$this->settings->slack_webhook_url = $this->slackWebhookUrl;
|
||||
|
||||
@@ -158,6 +157,7 @@ class Slack extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -170,6 +170,7 @@ class Slack extends Component
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -179,6 +180,8 @@ class Slack extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
refreshSession();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
|
||||
@@ -134,11 +134,10 @@ class Telegram extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->telegram_enabled = $this->telegramEnabled;
|
||||
$this->settings->telegram_token = $this->telegramToken;
|
||||
$this->settings->telegram_chat_id = $this->telegramChatId;
|
||||
@@ -223,6 +222,7 @@ class Telegram extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -235,6 +235,7 @@ class Telegram extends Component
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -264,6 +265,8 @@ class Telegram extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
refreshSession();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
|
||||
@@ -82,11 +82,10 @@ class Webhook extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->webhook_enabled = $this->webhookEnabled;
|
||||
$this->settings->webhook_url = $this->webhookUrl;
|
||||
|
||||
@@ -152,6 +151,7 @@ class Webhook extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -162,6 +162,7 @@ class Webhook extends Component
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->syncData(true);
|
||||
$this->saveModel();
|
||||
} catch (\Throwable $e) {
|
||||
@@ -171,6 +172,8 @@ class Webhook extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
|
||||
$this->syncData(true);
|
||||
refreshSession();
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class Advanced extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -330,7 +330,7 @@ class General extends Component
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -65,7 +65,7 @@ class Source extends Component
|
||||
$this->gitCommitSha = trim($this->gitCommitSha);
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -31,7 +31,7 @@ class Swarm extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -128,7 +128,7 @@ class BackupEdit extends Component
|
||||
$this->status = $database->status;
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->backup->enabled = $this->backupEnabled;
|
||||
|
||||
@@ -121,7 +121,7 @@ class General extends Component
|
||||
);
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -199,6 +199,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class General extends Component
|
||||
);
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -191,6 +191,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Health extends Component
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -118,7 +118,7 @@ class General extends Component
|
||||
);
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -196,6 +196,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -244,6 +244,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -237,6 +237,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -244,6 +244,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -240,6 +240,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
@@ -235,6 +235,7 @@ class General extends Component
|
||||
}
|
||||
$this->dispatch('databaseUpdated');
|
||||
} catch (\Throwable $e) {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->isPublic = ! $this->isPublic;
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class Edit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -48,7 +48,7 @@ class EnvironmentEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -46,7 +46,7 @@ class EditDomain extends Component
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -120,7 +120,7 @@ class FileStorage extends Component
|
||||
: route('project.application.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]);
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
if ($this->fileStorage->is_too_large) {
|
||||
|
||||
@@ -162,7 +162,7 @@ class Show extends Component
|
||||
$this->valuesLoaded = true;
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
||||
|
||||
@@ -86,7 +86,7 @@ class HealthChecks extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -102,7 +102,7 @@ class Add extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function saveScheduledTask()
|
||||
private function saveScheduledTask(): mixed
|
||||
{
|
||||
try {
|
||||
$task = new ScheduledTask;
|
||||
|
||||
@@ -87,7 +87,7 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -42,10 +42,9 @@ class Advanced extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
$this->server->settings->concurrent_builds = $this->concurrentBuilds;
|
||||
$this->server->settings->dynamic_timeout = $this->dynamicTimeout;
|
||||
@@ -67,6 +66,7 @@ class Advanced extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -81,6 +81,7 @@ class Advanced extends Component
|
||||
$this->serverDiskUsageCheckFrequency = $this->server->settings->getOriginal('server_disk_usage_check_frequency');
|
||||
throw new \Exception('Invalid Cron / Human expression for Disk Usage Check Frequency.');
|
||||
}
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -97,10 +97,9 @@ class DockerCleanup extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
$this->server->settings->force_docker_cleanup = $this->forceDockerCleanup;
|
||||
$this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency;
|
||||
@@ -122,6 +121,7 @@ class DockerCleanup extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -147,6 +147,7 @@ class DockerCleanup extends Component
|
||||
$this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency');
|
||||
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.');
|
||||
}
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -52,7 +52,7 @@ class LogDrains extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncDataNewRelic(bool $toModel = false)
|
||||
private function syncDataNewRelic(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled;
|
||||
@@ -65,7 +65,7 @@ class LogDrains extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncDataAxiom(bool $toModel = false)
|
||||
private function syncDataAxiom(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled;
|
||||
@@ -78,7 +78,7 @@ class LogDrains extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncDataCustom(bool $toModel = false)
|
||||
private function syncDataCustom(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled;
|
||||
@@ -91,7 +91,7 @@ class LogDrains extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false, ?string $type = null)
|
||||
private function syncData(bool $toModel = false, ?string $type = null): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->customValidation();
|
||||
|
||||
@@ -62,10 +62,9 @@ class TerminalAccess extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
// No other fields to sync for terminal access
|
||||
} else {
|
||||
|
||||
@@ -54,10 +54,9 @@ class Sentinel extends Component
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->validate();
|
||||
$this->server->settings->is_metrics_enabled = $this->isMetricsEnabled;
|
||||
$this->server->settings->sentinel_token = $this->sentinelToken;
|
||||
@@ -145,6 +144,7 @@ class Sentinel extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Sentinel settings updated. Restarting Sentinel.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -155,6 +155,7 @@ class Sentinel extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->restartSentinel();
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -230,12 +230,10 @@ class Show extends Component
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
$this->authorize('update', $this->server);
|
||||
$foundServer = Server::where('ip', $this->ip)
|
||||
->where('id', '!=', $this->server->id)
|
||||
->first();
|
||||
@@ -363,6 +361,7 @@ class Show extends Component
|
||||
public function checkLocalhostConnection()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
@@ -479,6 +478,7 @@ class Show extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -694,6 +694,7 @@ class Show extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Server settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -29,10 +29,9 @@ class Swarm extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->settings->is_swarm_manager = $this->isSwarmManager;
|
||||
$this->server->settings->is_swarm_worker = $this->isSwarmWorker;
|
||||
$this->server->settings->save();
|
||||
@@ -45,6 +44,7 @@ class Swarm extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -74,7 +74,7 @@ class SettingsEmail extends Component
|
||||
$this->testEmailAddress = auth()->user()->email;
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Exceptions\MethodNotFoundException;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -188,6 +189,21 @@ test('member cannot submit application advanced settings', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('the private application advanced syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$original = (bool) $this->application->settings->is_force_https_enabled;
|
||||
|
||||
$component = Livewire::test(ApplicationAdvanced::class, ['application' => $this->application])
|
||||
->set('isForceHttpsEnabled', ! $original);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
|
||||
expect((bool) $this->application->settings->fresh()->is_force_https_enabled)->toBe($original);
|
||||
});
|
||||
|
||||
// --- Application Rollback Livewire actions ---
|
||||
|
||||
test('member cannot save rollback settings', function () {
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Exceptions\MethodNotFoundException;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -43,13 +44,14 @@ test('member cannot send test notification on discord', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update discord notification settings', function () {
|
||||
test('the private discord syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(DiscordNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(DiscordNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('admin can update discord notification settings', function () {
|
||||
@@ -78,13 +80,14 @@ test('member cannot send test notification on slack', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update slack notification settings', function () {
|
||||
test('the private slack syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(SlackNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(SlackNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('admin can update slack notification settings', function () {
|
||||
@@ -114,13 +117,14 @@ test('member cannot send test notification on telegram', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update telegram notification settings', function () {
|
||||
test('the private telegram syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TelegramNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(TelegramNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('admin can update telegram notification settings', function () {
|
||||
@@ -138,7 +142,7 @@ test('telegram restart limit thread id accepts 255 characters', function () {
|
||||
|
||||
Livewire::test(TelegramNotification::class)
|
||||
->set('telegramNotificationsRestartLimitReachedThreadId', str_repeat('a', 255))
|
||||
->call('syncData', true)
|
||||
->call('submit')
|
||||
->assertHasNoErrors(['telegramNotificationsRestartLimitReachedThreadId']);
|
||||
|
||||
expect($this->team->telegramNotificationSettings->fresh()->telegram_notifications_restart_limit_reached_thread_id)
|
||||
@@ -151,8 +155,11 @@ test('telegram restart limit thread id rejects 256 characters', function () {
|
||||
|
||||
Livewire::test(TelegramNotification::class)
|
||||
->set('telegramNotificationsRestartLimitReachedThreadId', str_repeat('a', 256))
|
||||
->call('syncData', true)
|
||||
->assertHasErrors(['telegramNotificationsRestartLimitReachedThreadId' => 'max']);
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($this->team->telegramNotificationSettings->fresh()->telegram_notifications_restart_limit_reached_thread_id)
|
||||
->not->toBe(str_repeat('a', 256));
|
||||
});
|
||||
|
||||
test('member cannot view telegram thread ids', function () {
|
||||
@@ -226,13 +233,14 @@ test('member cannot send test email notification', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update email notification settings', function () {
|
||||
test('the private email syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(EmailNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('member cannot update smtp email transport directly', function () {
|
||||
@@ -330,13 +338,14 @@ test('member cannot send test notification on pushover', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update pushover notification settings', function () {
|
||||
test('the private pushover syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PushoverNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(PushoverNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('admin can update pushover notification settings', function () {
|
||||
@@ -365,13 +374,14 @@ test('member cannot send test notification on webhook', function () {
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update webhook notification settings', function () {
|
||||
test('the private webhook syncData helper is not remotely callable', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(WebhookNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
$component = Livewire::test(WebhookNotification::class);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
});
|
||||
|
||||
test('admin can update webhook notification settings', function () {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\ScheduledTask\Add;
|
||||
use App\Livewire\Project\Shared\ScheduledTask\Show;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Routing\Events\RouteMatched;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Livewire\Exceptions\MethodNotFoundException;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutVite();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->owner = User::factory()->create();
|
||||
$this->owner->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = $this->project->environments()->first();
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->task = ScheduledTask::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'application_id' => $this->application->id,
|
||||
'name' => 'validate-benign',
|
||||
'command' => 'echo scheduled-ok',
|
||||
'frequency' => '0 0 * * *',
|
||||
'timeout' => 300,
|
||||
'enabled' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Event::forget(RouteMatched::class);
|
||||
});
|
||||
|
||||
function bindApplicationScheduledTaskRoute(Application $application, ScheduledTask $task): void
|
||||
{
|
||||
$application->loadMissing('environment.project');
|
||||
|
||||
Event::listen(RouteMatched::class, function (RouteMatched $event) use ($application, $task): void {
|
||||
$event->route->setParameter('task_uuid', $task->uuid);
|
||||
$event->route->setParameter('project_uuid', $application->environment->project->uuid);
|
||||
$event->route->setParameter('environment_uuid', $application->environment->uuid);
|
||||
$event->route->setParameter('application_uuid', $application->uuid);
|
||||
});
|
||||
}
|
||||
|
||||
function originalScheduledTaskState(ScheduledTask $task): array
|
||||
{
|
||||
return $task->only(['name', 'command', 'frequency', 'timeout', 'enabled', 'container']);
|
||||
}
|
||||
|
||||
test('read-only member can view an existing scheduled task', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
bindApplicationScheduledTaskRoute($this->application, $this->task);
|
||||
|
||||
Livewire::test(Show::class)
|
||||
->assertSuccessful()
|
||||
->assertSet('name', 'validate-benign')
|
||||
->assertSet('command', 'echo scheduled-ok')
|
||||
->assertSet('frequency', '0 0 * * *');
|
||||
});
|
||||
|
||||
test('the private syncData helper is not remotely callable through Livewire', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
bindApplicationScheduledTaskRoute($this->application, $this->task);
|
||||
|
||||
$original = originalScheduledTaskState($this->task);
|
||||
|
||||
$component = Livewire::test(Show::class)
|
||||
->set('name', 'member-edited')
|
||||
->set('command', 'echo member-edited')
|
||||
->set('frequency', '* * * * *')
|
||||
->set('timeout', 120)
|
||||
->set('isEnabled', true);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
|
||||
expect(originalScheduledTaskState($this->task->fresh()))->toBe($original);
|
||||
});
|
||||
|
||||
test('read-only member cannot persist scheduled task changes through submit', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
bindApplicationScheduledTaskRoute($this->application, $this->task);
|
||||
|
||||
$original = originalScheduledTaskState($this->task);
|
||||
|
||||
Livewire::test(Show::class)
|
||||
->set('name', 'member-edited')
|
||||
->set('command', 'echo member-edited')
|
||||
->set('frequency', '* * * * *')
|
||||
->call('submit');
|
||||
|
||||
expect(originalScheduledTaskState($this->task->fresh()))->toBe($original);
|
||||
});
|
||||
|
||||
test('the private saveScheduledTask helper is not remotely callable through Livewire', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(Add::class, [
|
||||
'id' => (string) $this->application->id,
|
||||
'type' => 'application',
|
||||
'containerNames' => collect(),
|
||||
])
|
||||
->set('name', 'member-created')
|
||||
->set('command', 'id')
|
||||
->set('frequency', '* * * * *')
|
||||
->set('timeout', 300);
|
||||
|
||||
expect(fn () => $component->call('saveScheduledTask'))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
|
||||
expect(ScheduledTask::query()->where('name', 'member-created')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('read-only member cannot create a scheduled task through submit', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Add::class, [
|
||||
'id' => (string) $this->application->id,
|
||||
'type' => 'application',
|
||||
'containerNames' => collect(),
|
||||
])
|
||||
->set('name', 'member-created')
|
||||
->set('command', 'id')
|
||||
->set('frequency', '* * * * *')
|
||||
->set('timeout', 300)
|
||||
->call('submit');
|
||||
|
||||
expect(ScheduledTask::query()->where('name', 'member-created')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can persist scheduled task changes through submit', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
bindApplicationScheduledTaskRoute($this->application, $this->task);
|
||||
|
||||
Livewire::test(Show::class)
|
||||
->set('name', 'updated-by-admin')
|
||||
->set('command', 'echo admin-ok')
|
||||
->set('frequency', '0 1 * * *')
|
||||
->set('timeout', 600)
|
||||
->call('submit')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->task->refresh();
|
||||
|
||||
expect($this->task->name)->toBe('updated-by-admin')
|
||||
->and($this->task->command)->toBe('echo admin-ok')
|
||||
->and($this->task->frequency)->toBe('0 1 * * *')
|
||||
->and($this->task->timeout)->toBe(600);
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Livewire\Server\Create as ServerCreate;
|
||||
use App\Livewire\Server\Index as ServerIndex;
|
||||
use App\Livewire\Server\LogDrains;
|
||||
use App\Livewire\Server\Navbar as ServerNavbar;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -12,6 +13,7 @@ use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Exceptions\MethodNotFoundException;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -79,6 +81,20 @@ test('member can view server', function () {
|
||||
expect($this->member->can('view', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('the private log drain syncData helper is not remotely callable', function () {
|
||||
$this->withoutVite();
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(LogDrains::class, ['server_uuid' => $this->server->uuid])
|
||||
->set('isLogDrainAxiomEnabled', true);
|
||||
|
||||
expect(fn () => $component->call('syncData', true))
|
||||
->toThrow(MethodNotFoundException::class);
|
||||
|
||||
expect((bool) $this->server->settings->fresh()->is_logdrain_axiom_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: manageProxy ---
|
||||
|
||||
test('admin can manage proxy', function () {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Return_;
|
||||
use PhpParser\Node\Stmt\TryCatch;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\ParserFactory;
|
||||
|
||||
/** @return list<array{path: string, method: ClassMethod}> */
|
||||
function livewireSyncDataMethods(string $directory): array
|
||||
{
|
||||
$methods = [];
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
$parser = (new ParserFactory)->createForNewestSupportedVersion();
|
||||
$finder = new NodeFinder;
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if (! $file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
$path = $file->getPathname();
|
||||
$contents = file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
continue;
|
||||
}
|
||||
foreach ($finder->findInstanceOf($parser->parse($contents) ?? [], ClassMethod::class) as $method) {
|
||||
if (preg_match('/^sync(Data|DatabaseData|ApplicationData)/', $method->name->toString())) {
|
||||
$methods[] = ['path' => $path, 'method' => $method];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($methods, fn (array $a, array $b): int => [$a['path'], $a['method']->name->toString()] <=> [$b['path'], $b['method']->name->toString()]);
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
function livewireMethodCallName(MethodCall $call): ?string
|
||||
{
|
||||
return $call->name instanceof Identifier ? $call->name->toString() : null;
|
||||
}
|
||||
|
||||
function livewireIsWriteSyncCall(MethodCall $call, string $syncMethod): bool
|
||||
{
|
||||
if (livewireMethodCallName($call) !== $syncMethod) {
|
||||
return false;
|
||||
}
|
||||
foreach ($call->args as $position => $argument) {
|
||||
if (($position === 0 || $argument->name?->toString() === 'toModel') && $argument->value instanceof ConstFetch && $argument->value->name->toLowerString() === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param list<Node\Stmt> $statements @param list<int> $unauthorizedLines */
|
||||
function livewireScanStatements(array $statements, string $syncMethod, bool $authorized, array &$unauthorizedLines): bool
|
||||
{
|
||||
foreach ($statements as $statement) {
|
||||
$authorized = livewireScanNode($statement, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/** @param list<int> $unauthorizedLines */
|
||||
function livewireScanNode(mixed $node, string $syncMethod, bool $authorized, array &$unauthorizedLines): bool
|
||||
{
|
||||
if (! $node instanceof Node && ! is_array($node)) {
|
||||
return $authorized;
|
||||
}
|
||||
if (is_array($node)) {
|
||||
return livewireScanStatements($node, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
if ($node instanceof TryCatch) {
|
||||
$tryAuthorized = livewireScanStatements($node->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
$allCatchesTerminate = $node->catches !== [];
|
||||
foreach ($node->catches as $catch) {
|
||||
livewireScanStatements($catch->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
$allCatchesTerminate = $allCatchesTerminate && collect($catch->stmts)->contains(fn (Node\Stmt $statement): bool => $statement instanceof Return_);
|
||||
}
|
||||
if ($node->finally !== null) {
|
||||
livewireScanStatements($node->finally->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
|
||||
return $allCatchesTerminate ? $tryAuthorized : $authorized;
|
||||
}
|
||||
if ($node instanceof If_) {
|
||||
livewireScanNode($node->cond, $syncMethod, $authorized, $unauthorizedLines);
|
||||
livewireScanStatements($node->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
foreach ($node->elseifs as $elseif) {
|
||||
livewireScanNode($elseif->cond, $syncMethod, $authorized, $unauthorizedLines);
|
||||
livewireScanStatements($elseif->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
if ($node->else !== null) {
|
||||
livewireScanStatements($node->else->stmts, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
if ($node instanceof MethodCall) {
|
||||
$name = livewireMethodCallName($node);
|
||||
if ($name === 'authorize') {
|
||||
return true;
|
||||
}
|
||||
if (livewireIsWriteSyncCall($node, $syncMethod) && ! $authorized) {
|
||||
$unauthorizedLines[] = $node->getStartLine();
|
||||
}
|
||||
}
|
||||
foreach ($node->getSubNodeNames() as $name) {
|
||||
$authorized = livewireScanNode($node->{$name}, $syncMethod, $authorized, $unauthorizedLines);
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
function livewireUnauthorizedSyncWriteLines(ClassMethod $method, string $syncMethod): array
|
||||
{
|
||||
$lines = [];
|
||||
livewireScanStatements($method->stmts ?? [], $syncMethod, false, $lines);
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
function livewireMethodFromSource(string $source, string $method): ClassMethod
|
||||
{
|
||||
$nodes = (new ParserFactory)->createForNewestSupportedVersion()->parse($source) ?? [];
|
||||
|
||||
return (new NodeFinder)->findFirst($nodes, fn (Node $node): bool => $node instanceof ClassMethod && $node->name->toString() === $method);
|
||||
}
|
||||
|
||||
it('keeps every Livewire syncData helper private', function () {
|
||||
$violations = [];
|
||||
foreach (livewireSyncDataMethods(dirname(__DIR__, 2).'/app/Livewire') as $syncMethod) {
|
||||
if (! $syncMethod['method']->isPrivate()) {
|
||||
$relative = str_replace(dirname(__DIR__, 2).'/', '', $syncMethod['path']);
|
||||
$violations[] = "{$relative}::{$syncMethod['method']->name}() is not private";
|
||||
}
|
||||
}
|
||||
expect($violations)->toBeEmpty("Livewire syncData helpers must be private:\n".implode("\n", $violations));
|
||||
});
|
||||
|
||||
it('discovers files that only define syncApplicationData helpers', function () {
|
||||
$directory = sys_get_temp_dir().'/livewire-sync-'.uniqid();
|
||||
mkdir($directory);
|
||||
file_put_contents($directory.'/Example.php', '<?php class Example { private function syncApplicationData(bool $toModel = false) {} }');
|
||||
|
||||
$methods = livewireSyncDataMethods($directory);
|
||||
|
||||
unlink($directory.'/Example.php');
|
||||
rmdir($directory);
|
||||
|
||||
expect($methods)->toHaveCount(1)
|
||||
->and($methods[0]['method']->name->toString())->toBe('syncApplicationData');
|
||||
});
|
||||
|
||||
it('detects named write arguments and does not carry authorization into catch paths', function () {
|
||||
$method = livewireMethodFromSource(<<<'PHP'
|
||||
<?php
|
||||
class Example {
|
||||
public function instantSave() {
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->syncData(toModel: true);
|
||||
} catch (Throwable $exception) {
|
||||
$this->syncData(toModel: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
PHP, 'instantSave');
|
||||
|
||||
expect(livewireUnauthorizedSyncWriteLines($method, 'syncData'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('authorizes before every syncData write call', function () {
|
||||
$violations = [];
|
||||
$root = dirname(__DIR__, 2);
|
||||
foreach (livewireSyncDataMethods($root.'/app/Livewire') as $syncMethod) {
|
||||
$contents = file_get_contents($syncMethod['path']);
|
||||
$nodes = (new ParserFactory)->createForNewestSupportedVersion()->parse($contents) ?? [];
|
||||
foreach ((new NodeFinder)->findInstanceOf($nodes, ClassMethod::class) as $caller) {
|
||||
foreach (livewireUnauthorizedSyncWriteLines($caller, $syncMethod['method']->name->toString()) as $line) {
|
||||
$relative = str_replace($root.'/', '', $syncMethod['path']);
|
||||
$violations[] = "{$relative}:{$line}::{$caller->name}() calls {$syncMethod['method']->name}(true) without prior authorization";
|
||||
}
|
||||
}
|
||||
}
|
||||
expect($violations)->toBeEmpty("Missing authorization before syncData write calls:\n".implode("\n", array_unique($violations)));
|
||||
});
|
||||
Reference in New Issue
Block a user