CertificationRequestService.php
1 <?php 2 3 namespace App\Http\Services; 4 5 use App\Enums\EntityTypeEnum; 6 use App\Enums\UsageDimensionsEnum; 7 use App\Models\Country; 8 use App\Models\DocumentType; 9 use App\Models\Dossier; 10 use App\Models\DossierType; 11 use App\Models\Entity; 12 use App\Models\Norm; 13 use App\Models\NormScope; 14 use App\Models\Role; 15 use App\Models\Taxonomy; 16 use App\Models\Template; 17 use App\Models\TOE; 18 use App\Models\Workbook; 19 use Exception; 20 use Illuminate\Support\Facades\Auth; 21 use Illuminate\Support\Facades\DB; 22 use Illuminate\Support\Facades\Storage; 23 use Illuminate\Support\Str; 24 use Symfony\Component\Process\Process; 25 use ZipArchive; 26 27 function getVersion($str) 28 { 29 preg_match("/(?:version|v)\s*((?:[0-9]+\.?)+)/i", $str, $matches); 30 try { 31 return $matches[1]; 32 } catch (Exception) { 33 return '1'; 34 } 35 } 36 37 class CertificationRequestService 38 { 39 public string $dossierPath; 40 public string $version; 41 42 public Dossier $dossier; 43 public array $data = []; 44 public string|null $request; 45 public int $userId; 46 47 public function __construct(string $path, int $userId = null) 48 { 49 $this->userId = $userId ?? Auth::user()->id; 50 $this->request = $this->getFor001($path); 51 } 52 53 private function getFor001(string $path) 54 { 55 if (!Storage::exists($path)) { 56 throw new Exception('File not found'); 57 } 58 59 $for001 = match (true) { 60 Str::endsWith($path, '.pdf') => $path, 61 default => self::findRequestFile($this->unzip($path)), 62 }; 63 64 return $for001; 65 } 66 67 public function readAndValidate(): array 68 { 69 if (!$this->request) { 70 return ['FOR001' => ['Missing FOR-001 file']]; 71 } 72 73 $format = $this->getFormat($this->request); 74 $errors = match ($format) { 75 'ccn' => $this->readCCNFormData($this->request), 76 'sgoc' => $this->readSGOCFormData($this->request), 77 default => ['format' => ['Invalid FOR-001 format']], 78 }; 79 80 return $errors; 81 } 82 83 public static function getFormat(string $path): ?string 84 { 85 $process = new Process(['pdftotext', Storage::path($path), '-']); 86 $process->run(); 87 88 if (!$process->isSuccessful()) { 89 return null; 90 } 91 92 $text = $process->getOutput(); 93 94 $toFind = "OC/CCN/CNI"; 95 if (Str::contains($text, $toFind)) { 96 return 'ccn'; 97 } 98 99 // This field only shows up in the new format 100 $toFind = "Dimensiones sectoriales"; 101 if (Str::contains($text, $toFind)) { 102 return 'sgoc'; 103 } 104 105 return null; 106 } 107 108 private function findScope(Norm $norm, string $scopeText): NormScope | Null 109 { 110 $trimmed = rtrim($scopeText, '+'); 111 return $norm->scopes() 112 ->where('name', $trimmed) 113 ->first(); 114 } 115 116 private function parseAdditionalComponents(string $componentsText): string 117 { 118 $components = explode(', ', $componentsText); 119 $filtered = array_filter($components, function ($component) { 120 return $component !== '-'; 121 }); 122 $capitalized = array_map('strtoupper', $filtered); 123 124 return implode(', ', $capitalized); 125 } 126 127 private function getModoName(array $form): string 128 { 129 return match (true) { 130 $form["Mantenimiento_check"] !== "Off" => "Maintenance", 131 $form["Renovacion_check"] !== "Off" => "Renewal", 132 $form["Recertificacion_check"] !== "Off" => "Recertification", 133 default => "New", 134 }; 135 } 136 137 private static function getToeTypesFromString(string $str): array 138 { 139 $foundTypes = []; 140 foreach (UsageDimensionsEnum::toArray() as $type) { 141 if (Str::contains($str, $type)) { 142 $foundTypes[] = $type; 143 } 144 } 145 146 return $foundTypes; 147 } 148 149 public function readCCNFormData(string $path): array 150 { 151 $form = self::readForm($path); 152 153 // get modo 154 $type = DossierType::where('name', 'Certification')->first(); 155 $modo = $type->children->where('name', $this->getModoName($form))->first(); 156 $subtype = null; 157 158 // get norm 159 $norm = CertificationRequestService::getNorm($form['Norm_producto']); 160 $normGroup = $norm->normGroup->name ?? null; 161 if ($normGroup) { 162 $subtype = $modo 163 ->children() 164 ->where('name', 'like', '%' . $normGroup . '%') 165 ->first(); 166 } 167 $scope = $this->findScope($norm, $form['Scope_producto']); 168 $additionalComponents = $this->parseAdditionalComponents($form['Componentes_producto']); 169 170 // transform ccn data to sgoc valid data 171 $this->data = [ 172 "applicant_name" => $form['Applicant.reason'], 173 "applicant_trade_name" => $form['Applicant.reason'], 174 "applicant_nif" => $form['Applicant.cif'], 175 "applicant_corporate_identity" => $form['Applicant.juri'], 176 "applicant_city_name" => $form['Applicant.PointOfContact.city'], 177 "applicant_zip_code" => $form['Applicant.PointOfContact.postal_code'], 178 "applicant_phone" => $form['Applicant.PointOfContact.phone'], 179 "applicant_email" => $form['Applicant.PointOfContact.email'], 180 "applicant_address" => $form['Applicant.PointOfContact.address'], 181 "applicant_country" => $form['Applicant.PointOfContact.country'], 182 183 "toe_alias" => $form['TOE.name'], 184 "toe_name" => $form['TOE.name'], 185 "toe_version" => $form['TOE.version'], 186 "toe_support_period" => null, 187 "toe_scope" => null, 188 "toe_description" => "", 189 "toe_technological_dimensions" => null, 190 "toe_security_info_link" => null, 191 "toe_description" => null, 192 "toe_sectorial_dimensions" => null, 193 "toe_usage_dimensions" => self::getToeTypesFromString($form['Tipo_producto']), 194 "toe_public_vulnerabilities_link" => null, 195 "toe_vulnerability_notifications_via" => null, 196 "toe_usague_guide_link" => null, 197 "toe_vulnerability_contact_link 2" => null, 198 199 "dossier_norm_version" => $norm->name, 200 "dossier_type" => $type->name ?? null, 201 "dossier_modo" => $modo->name ?? null, 202 "dossier_subtype" => $subtype->name ?? null, 203 "dossier_evaluation_level" => $scope->name ?? null, 204 "dossier_additional_components" => $additionalComponents, 205 "dossier_scope" => $scope->name ?? null, 206 "dossier_additional_components" => $additionalComponents, 207 "ref_st" => $form['STReference'], 208 "ref_strr" => $form['STRevisionResult'], 209 "ref_previous_dossier" => $form['DossierReference'], 210 211 "laboratory_name" => $form['Laboratory.name'], 212 "laboratory_trade_name" => $form['Laboratory.name'], 213 "laboratory_nif" => null, 214 "laboratory_corporate_identity" => null, 215 "laboratory_city_name" => null, 216 "laboratory_zip_code" => null, 217 "laboratory_address" => "", 218 "laboratory_phone" => "", 219 "laboratory_email" => null, 220 "laboratory_notification_number" => null, 221 "laboratory_authorization_number" => null, 222 223 "manufacturer_name" => $form['Manufacturer.name'], 224 "manufacturer_city_name" => $form['Manufacturer.PointOfContact.city'], 225 "manufacturer_zip_code" => $form['Manufacturer.PointOfContact.postal_code'], 226 "manufacturer_address" => $form['Manufacturer.PointOfContact.address'], 227 "manufacturer_phone" => $form['Manufacturer.PointOfContact.phone'], 228 "manufacturer_country" => $form['Manufacturer.PointOfContact.country'], 229 ]; 230 231 // validate data 232 return (new CertificationRequestValidator($this->data))->validate(); 233 } 234 235 // NOTE: this method is not used anymore 236 public function readSGOCFormData(string $path): array 237 { 238 $this->data = self::readForm($path); 239 return (new CertificationRequestValidator($this->data))->validate(); 240 } 241 242 public function unzip($path) 243 { 244 if (!Storage::exists($path)) { 245 throw new Exception('File not found'); 246 } 247 248 $tmp = "tmp/" . Str::random(30); 249 250 $zip = new ZipArchive; 251 $zip->open(Storage::path($path)); 252 $zip->extractTo(Storage::path($tmp)); 253 254 return $tmp; 255 } 256 257 public static function readForm(string $path): array 258 { 259 if (!Storage::exists($path)) { 260 throw new Exception('File not found'); 261 } 262 263 return PdfFormReaderService::read($path); 264 } 265 266 public static function getNorm(string $search): ?Norm 267 { 268 // map ccn provided pdf values to their database corresponding values 269 $search = match ($search) { 270 'LINCE - Certificación Nacional Esencial de Seguridad, versión 2.0' => 'LINCE 2.0.0', 271 'Common Criteria version 3.1 release 5' => 'Common Criteria v3.1R5', 272 'CC:2022 Release 1' => 'Common Criteria 2022 R1', 273 'ITSEC/ITSEM' => 'ITSEC', // FIXME: ITSEC/ITSEM is defined as two norms in the database! 274 'ISO/IEC 19790:2012' => 'ISO 19790:2012', 275 default => $search, 276 }; 277 278 $norm = Norm::select('id', 'version', 'norm_group_id')->get() 279 ->filter(function ($norm) use ($search) { 280 return $norm->name == $search; 281 }) 282 ->first(); 283 284 if (!$norm) { 285 throw new Exception('Norm not found'); 286 } 287 288 return $norm; 289 } 290 291 public function build(bool $activate = false): Dossier 292 { 293 DB::beginTransaction(); 294 295 // find stuff for the dossier 296 $components = []; 297 $componentIds = []; 298 if ($this->data['dossier_additional_components'] !== "") { 299 $components = explode(',', $this->data['dossier_additional_components']); 300 $componentIds = Workbook::whereIn('name', $components)->pluck('id'); 301 } 302 303 $type = DossierType::where('name', $this->data['dossier_type'])->first(); 304 $subtype = $type->subtypes()->where('name', $this->data['dossier_subtype'])->first(); 305 $taxonomy = Taxonomy::where('dossier_type_id', $subtype->id)->first(); 306 $norm = self::getNorm($this->data['dossier_norm_version']); 307 308 $normScope = null; 309 if ($this->data['dossier_evaluation_level']) { 310 $normScope = $this->findScope($norm, $this->data['dossier_evaluation_level']); 311 } 312 313 $componentIds = $norm->workbooks()->whereIn('name', explode(', ', $this->data['dossier_additional_components']))->pluck('id'); 314 315 // Build relations first 316 $toe = $this->buildToe(); 317 $applicant = $this->buildApplicant(); 318 $laboratory = $this->buildLaboratory(); 319 $manufacturer = $this->buildManufacturer(); 320 321 $this->dossier = DossierService::create([ 322 'dossier_type_id' => $subtype->id, 323 'active' => 0, 324 'norm_id' => $norm->id, 325 'norm_scope_id' => $normScope->id ?? null, 326 'toe_id' => $toe->id, 327 'applicant_id' => $applicant->id, 328 'sponsor_id' => $applicant->id, 329 'manufacturer_id' => $manufacturer->id, 330 'laboratory_id' => $laboratory->id, 331 'principal_certifier_id' => null, 332 'evaluators' => [], 333 'secondary_certifiers' => [], 334 'external_certifiers' => [], 335 'experts' => [], 336 'informed_staff' => [], 337 'related_workbooks' => $componentIds, 338 ], $this->userId); 339 340 $this->dossier->active = $activate; 341 $this->dossier->save(); 342 343 if ($taxonomy) { 344 DossierService::addTaxonomy($this->dossier, $taxonomy); 345 } 346 DB::commit(); 347 348 return $this->dossier; 349 } 350 351 public static function findRequestFile(string $path) 352 { 353 $matches = collect(Storage::allFiles($path))->filter(function ($file) { 354 return Str::contains($file, "FOR-001") and Str::endsWith($file, '.pdf'); 355 }); 356 357 return $matches->first(); 358 } 359 360 public function createNotification($document, $dossierId = null) 361 { 362 $data = [ 363 'type_id' => DocumentType::where('code', 'NOT')->first()->id, 364 'template_id' => Template::where('name', 'like', 'NOT-01%')->first()->id, 365 'dossier_id' => $dossierId ?? $this->dossier->id, 366 'meet_id' => null, 367 ]; 368 369 // Use technical manager if exists, else use root 370 $user = Role::where('name', 'technical_manager')->first()->users->first(); 371 if (!$user) { 372 $user = Role::where('name', 'root')->first()->users->first(); 373 } 374 375 DocumentService::createWithTemplate($data, $user->id, $document->id); 376 } 377 378 private function buildToe(): TOE 379 { 380 // TODO: create toe from data 381 return TOE::firstOrCreate( 382 [ 383 'name' => $this->data['toe_name'], 384 ], 385 [ 386 'alias' => $this->data['toe_alias'], 387 'commercial_name' => $this->data['toe_name'], 388 'version' => $this->data['toe_version'], 389 'description' => $this->data['toe_description'], 390 'period_security_support' => $this->data['toe_support_period'], 391 'cyber_security_info_url' => $this->data['toe_security_info_link'], 392 'vulnerability_nofifications' => $this->data['toe_vulnerability_notifications_via'], 393 'guidance_url' => $this->data['toe_usague_guide_link'], 394 'evaluation_target' => '?', 395 'evaluation_scope' => $this->data['toe_scope'], 396 'sectorial_dimensions' => explode('; ', $this->data['toe_sectorial_dimensions']), 397 'usage_dimension' => $this->data['toe_usage_dimensions'], 398 'technological_dimensions' => explode('; ', $this->data['toe_technological_dimensions']), 399 ] 400 ); 401 } 402 403 private function buildApplicant(): Entity 404 { 405 try { 406 DB::beginTransaction(); 407 408 $entity = Entity::where('name', $this->data['applicant_name'])->where('entity_type_id', EntityTypeEnum::company())->firstOr(function () { 409 $entity = Entity::create([ 410 'entity_type_id' => EntityTypeEnum::company(), 411 'name' => $this->data['applicant_name'], 412 'legal_name' => $this->data['applicant_trade_name'], 413 'code' => '', 414 'nif' => $this->data['applicant_nif'], 415 ]); 416 417 $countryId = Country::whereName($this->data['applicant_country']) 418 ->first()->id ?? null; 419 420 $entity->contactInfo()->create([ 421 'address' => $this->data['applicant_address'], 422 'city' => $this->data['applicant_city_name'], 423 'postal_code' => $this->data['applicant_zip_code'], 424 'phone' => $this->data['applicant_phone'], 425 'email' => $this->data['applicant_email'], 426 'country_id' => $countryId, 427 ]); 428 429 return $entity; 430 }); 431 432 DB::commit(); 433 } catch (Exception $e) { 434 logger($e); 435 DB::rollBack(); 436 } 437 438 439 return $entity; 440 } 441 442 private function buildManufacturer(): Entity 443 { 444 try { 445 $countryId = Country 446 ::whereName($this->data['manufacturer_country'] ?? '') 447 ->value('id'); 448 449 $entity = Entity 450 ::where('name', $this->data['manufacturer_name'] ?? '') 451 ->where('entity_type_id', EntityTypeEnum::company()) 452 ->whereHas('contactInfo', function ($query) { 453 $query->where('address', $this->data['manufacturer_address'] ?? ''); 454 }) 455 ->first(); 456 457 if (!$entity) { 458 $entity = Entity::create([ 459 'entity_type_id' => EntityTypeEnum::company(), 460 'name' => $this->data['manufacturer_name'] ?? '', 461 ]); 462 463 $entity->contactInfo()->create([ 464 'address' => $this->data['manufacturer_address'] ?? '', 465 'city' => $this->data['manufacturer_city_name'] ?? '', 466 'postal_code' => $this->data['manufacturer_zip_code'] ?? '', 467 'phone' => $this->data['manufacturer_phone'] ?? '', 468 'email' => $this->data['manufacturer_email'] ?? '', 469 'country_id' => $countryId, 470 ]); 471 } 472 473 DB::commit(); 474 } catch (Exception $e) { 475 logger($e); 476 DB::rollBack(); 477 } 478 479 return $entity; 480 } 481 482 private function buildLaboratory(): Entity 483 { 484 try { 485 DB::beginTransaction(); 486 487 $entity = Entity::where('name', $this->data['laboratory_name'])->where('entity_type_id', EntityTypeEnum::laboratory())->firstOr(function () { 488 $entity = Entity::create([ 489 'entity_type_id' => EntityTypeEnum::laboratory(), 490 'name' => $this->data['laboratory_name'], 491 'legal_name' => $this->data['laboratory_trade_name'], 492 'code' => '', 493 'nif' => $this->data['laboratory_nif'], 494 'notification_number' => $this->data['laboratory_notification_number'], 495 'authorization_number' => $this->data['laboratory_authorization_number'], 496 ]); 497 498 $entity->contactInfo()->create([ 499 'address' => $this->data['laboratory_address'], 500 'city' => $this->data['laboratory_city_name'], 501 'postal_code' => $this->data['laboratory_zip_code'], 502 'phone' => $this->data['laboratory_phone'], 503 'email' => $this->data['laboratory_email'], 504 'country_id' => null, // TODO: put country in pdf, or make nullable 505 ]); 506 507 return $entity; 508 }); 509 510 DB::commit(); 511 } catch (Exception $e) { 512 logger($e); 513 DB::rollBack(); 514 } 515 516 return $entity; 517 } 518 }