/ app / Http / Services / CertificationRequestService.php
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  }