Files
sam-api/app/Http/Requests/Api/V1/Design/ModelFormulaFormRequest.php

264 lines
9.5 KiB
PHP
Raw Normal View History

<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Design\ModelParameter;
class ModelFormulaFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$modelId = $this->route('modelId');
$formulaId = $this->route('formulaId');
$rules = [
'formula_name' => [
'required',
'string',
'max:100',
'regex:/^[a-zA-Z][a-zA-Z0-9_\s]*$/',
Rule::unique('model_formulas')
->where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
],
'formula_expression' => ['required', 'string', 'max:1000'],
'unit' => ['nullable', 'string', 'max:20'],
'description' => ['nullable', 'string', 'max:500'],
'calculation_order' => ['integer', 'min:0'],
'dependencies' => ['nullable', 'array'],
'dependencies.*' => ['string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
];
// For update requests, ignore current record in unique validation
if ($formulaId) {
$rules['formula_name'][4] = $rules['formula_name'][4]->ignore($formulaId);
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'formula_name.required' => '공식 이름은 필수입니다.',
'formula_name.regex' => '공식 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어, 공백만 사용할 수 있습니다.',
'formula_name.unique' => '해당 모델에 이미 동일한 공식 이름이 존재합니다.',
'formula_expression.required' => '공식 표현식은 필수입니다.',
'formula_expression.max' => '공식 표현식은 1000자를 초과할 수 없습니다.',
'unit.max' => '단위는 20자를 초과할 수 없습니다.',
'description.max' => '설명은 500자를 초과할 수 없습니다.',
'calculation_order.min' => '계산 순서는 0 이상이어야 합니다.',
'dependencies.array' => '의존성은 배열 형태여야 합니다.',
'dependencies.*.regex' => '의존성 변수명은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'formula_name' => '공식 이름',
'formula_expression' => '공식 표현식',
'unit' => '단위',
'description' => '설명',
'calculation_order' => '계산 순서',
'dependencies' => '의존성',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'calculation_order' => $this->integer('calculation_order', 0),
]);
// Convert dependencies to array if it's a string
if ($this->has('dependencies') && is_string($this->input('dependencies'))) {
$dependencies = json_decode($this->input('dependencies'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['dependencies' => $dependencies]);
}
}
// Clean up formula expression - remove extra whitespace
if ($this->has('formula_expression')) {
$expression = preg_replace('/\s+/', ' ', trim($this->input('formula_expression')));
$this->merge(['formula_expression' => $expression]);
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
$this->validateFormulaExpression($validator);
$this->validateDependencies($validator);
$this->validateNoCircularDependency($validator);
});
}
/**
* Validate formula expression syntax.
*/
private function validateFormulaExpression($validator): void
{
$expression = $this->input('formula_expression');
if (!$expression) {
return;
}
// Check for potentially dangerous characters or functions
$dangerousPatterns = [
'/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i',
'/[;{}]/', // Semicolons and braces
'/\$[a-zA-Z_]/', // PHP variables
'/\bfunction\s*\(/i', // Function definitions
];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $expression)) {
$validator->errors()->add('formula_expression', '공식 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.');
return;
}
}
// Validate mathematical expression format
if (!$this->isValidMathExpression($expression)) {
$validator->errors()->add('formula_expression', '공식 표현식의 형식이 올바르지 않습니다. 수학 연산자와 변수명만 사용할 수 있습니다.');
}
// Extract variables from expression and validate they exist as parameters
$this->validateExpressionVariables($validator, $expression);
}
/**
* Check if expression contains valid mathematical operations.
*/
private function isValidMathExpression(string $expression): bool
{
// Allow: numbers, variables, basic math operators, parentheses, math functions
$allowedPattern = '/^[a-zA-Z0-9_\s\+\-\*\/\(\)\.\,]+$/';
// Allow common math functions
$mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min'];
$functionPattern = '/\b(' . implode('|', $mathFunctions) . ')\s*\(/i';
// Remove math functions for basic pattern check
$cleanExpression = preg_replace($functionPattern, '', $expression);
return preg_match($allowedPattern, $cleanExpression);
}
/**
* Validate that variables in expression exist as model parameters.
*/
private function validateExpressionVariables($validator, string $expression): void
{
$modelId = $this->route('modelId');
// Extract variable names from expression
preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches);
$variables = $matches[0];
// Remove math functions from variables
$mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min'];
$variables = array_diff($variables, $mathFunctions);
if (empty($variables)) {
return;
}
// Get existing parameters for this model
$existingParameters = ModelParameter::where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->pluck('parameter_name')
->toArray();
// Check for undefined variables
$undefinedVariables = array_diff($variables, $existingParameters);
if (!empty($undefinedVariables)) {
$validator->errors()->add('formula_expression',
'공식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables)
);
}
}
/**
* Validate dependencies array.
*/
private function validateDependencies($validator): void
{
$dependencies = $this->input('dependencies', []);
$modelId = $this->route('modelId');
if (empty($dependencies)) {
return;
}
// Get existing parameters for this model
$existingParameters = ModelParameter::where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->pluck('parameter_name')
->toArray();
// Check that all dependencies exist as parameters
$invalidDependencies = array_diff($dependencies, $existingParameters);
if (!empty($invalidDependencies)) {
$validator->errors()->add('dependencies',
'존재하지 않는 매개변수가 의존성에 포함되어 있습니다: ' . implode(', ', $invalidDependencies)
);
}
}
/**
* Validate there's no circular dependency.
*/
private function validateNoCircularDependency($validator): void
{
$dependencies = $this->input('dependencies', []);
$formulaName = $this->input('formula_name');
if (empty($dependencies) || !$formulaName) {
return;
}
// Check for direct self-reference
if (in_array($formulaName, $dependencies)) {
$validator->errors()->add('dependencies', '공식이 자기 자신을 참조할 수 없습니다.');
return;
}
// For more complex circular dependency check, this would require
// analyzing all formulas in the model - simplified version here
// In production, implement full dependency graph analysis
}
}