Replace all uses of `forceFill`, `forceCreate`, and `forceFill` with their non-force equivalents across models, actions, controllers, and Livewire components. Add explicit `$fillable` arrays to all affected Eloquent models to enforce mass assignment protection. Add ModelFillableCreationTest and ModelFillableRegressionTest to verify that model creation respects fillable constraints and prevent regressions.
195 lines
5.6 KiB
PHP
195 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use OpenApi\Attributes as OA;
|
|
|
|
#[OA\Schema(
|
|
description: 'Project model',
|
|
type: 'object',
|
|
properties: [
|
|
'id' => ['type' => 'integer'],
|
|
'application_id' => ['type' => 'string'],
|
|
'deployment_uuid' => ['type' => 'string'],
|
|
'pull_request_id' => ['type' => 'integer'],
|
|
'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true],
|
|
'force_rebuild' => ['type' => 'boolean'],
|
|
'commit' => ['type' => 'string'],
|
|
'status' => ['type' => 'string'],
|
|
'is_webhook' => ['type' => 'boolean'],
|
|
'is_api' => ['type' => 'boolean'],
|
|
'created_at' => ['type' => 'string'],
|
|
'updated_at' => ['type' => 'string'],
|
|
'logs' => ['type' => 'string'],
|
|
'current_process_id' => ['type' => 'string'],
|
|
'restart_only' => ['type' => 'boolean'],
|
|
'git_type' => ['type' => 'string'],
|
|
'server_id' => ['type' => 'integer'],
|
|
'application_name' => ['type' => 'string'],
|
|
'server_name' => ['type' => 'string'],
|
|
'deployment_url' => ['type' => 'string'],
|
|
'destination_id' => ['type' => 'string'],
|
|
'only_this_server' => ['type' => 'boolean'],
|
|
'rollback' => ['type' => 'boolean'],
|
|
'commit_message' => ['type' => 'string'],
|
|
],
|
|
)]
|
|
class ApplicationDeploymentQueue extends Model
|
|
{
|
|
protected $fillable = [
|
|
'application_id',
|
|
'deployment_uuid',
|
|
'pull_request_id',
|
|
'docker_registry_image_tag',
|
|
'force_rebuild',
|
|
'commit',
|
|
'status',
|
|
'is_webhook',
|
|
'logs',
|
|
'current_process_id',
|
|
'restart_only',
|
|
'git_type',
|
|
'server_id',
|
|
'application_name',
|
|
'server_name',
|
|
'deployment_url',
|
|
'destination_id',
|
|
'only_this_server',
|
|
'rollback',
|
|
'commit_message',
|
|
'is_api',
|
|
'build_server_id',
|
|
'horizon_job_id',
|
|
'horizon_job_worker',
|
|
'finished_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'pull_request_id' => 'integer',
|
|
'finished_at' => 'datetime',
|
|
];
|
|
|
|
public function application()
|
|
{
|
|
return $this->belongsTo(Application::class);
|
|
}
|
|
|
|
public function server(): Attribute
|
|
{
|
|
return Attribute::make(
|
|
get: fn () => Server::find($this->server_id),
|
|
);
|
|
}
|
|
|
|
public function setStatus(string $status)
|
|
{
|
|
$this->update([
|
|
'status' => $status,
|
|
]);
|
|
}
|
|
|
|
public function getOutput($name)
|
|
{
|
|
if (! $this->logs) {
|
|
return null;
|
|
}
|
|
|
|
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
|
|
}
|
|
|
|
public function getHorizonJobStatus()
|
|
{
|
|
return getJobStatus($this->horizon_job_id);
|
|
}
|
|
|
|
public function commitMessage()
|
|
{
|
|
if (empty($this->commit_message) || is_null($this->commit_message)) {
|
|
return null;
|
|
}
|
|
|
|
return str($this->commit_message)->value();
|
|
}
|
|
|
|
private function redactSensitiveInfo($text)
|
|
{
|
|
$text = remove_iip($text);
|
|
|
|
$app = $this->application;
|
|
if (! $app) {
|
|
return $text;
|
|
}
|
|
|
|
$lockedVars = collect([]);
|
|
|
|
if ($app->environment_variables) {
|
|
$lockedVars = $lockedVars->merge(
|
|
$app->environment_variables
|
|
->where('is_shown_once', true)
|
|
->pluck('real_value', 'key')
|
|
->filter()
|
|
);
|
|
}
|
|
|
|
if ($this->pull_request_id !== 0 && $app->environment_variables_preview) {
|
|
$lockedVars = $lockedVars->merge(
|
|
$app->environment_variables_preview
|
|
->where('is_shown_once', true)
|
|
->pluck('real_value', 'key')
|
|
->filter()
|
|
);
|
|
}
|
|
|
|
foreach ($lockedVars as $key => $value) {
|
|
$escapedValue = preg_quote($value, '/');
|
|
$text = preg_replace(
|
|
'/'.$escapedValue.'/',
|
|
REDACTED,
|
|
$text
|
|
);
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
|
|
{
|
|
if ($type === 'error') {
|
|
$type = 'stderr';
|
|
}
|
|
$message = str($message)->trim();
|
|
if ($message->startsWith('╔')) {
|
|
$message = "\n".$message;
|
|
}
|
|
$newLogEntry = [
|
|
'command' => null,
|
|
'output' => $this->redactSensitiveInfo($message),
|
|
'type' => $type,
|
|
'timestamp' => Carbon::now('UTC'),
|
|
'hidden' => $hidden,
|
|
'batch' => 1,
|
|
];
|
|
|
|
// Use a transaction to ensure atomicity
|
|
DB::transaction(function () use ($newLogEntry) {
|
|
// Reload the model to get the latest logs
|
|
$this->refresh();
|
|
|
|
if ($this->logs) {
|
|
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
|
$newLogEntry['order'] = count($previousLogs) + 1;
|
|
$previousLogs[] = $newLogEntry;
|
|
$this->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
|
|
} else {
|
|
$this->logs = json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
// Save without triggering events to prevent potential race conditions
|
|
$this->saveQuietly();
|
|
});
|
|
}
|
|
}
|