/ app / Http / Controllers / InboxSftpFileForDownload.php
InboxSftpFileForDownload.php
  1  <?php
  2  
  3  namespace App\Http\Controllers;
  4  
  5  use App\Enums\DossierLevelEnum;
  6  use App\Enums\InboxFileTypeEnum;
  7  use App\Helpers\InboxFileSaver;
  8  use App\Http\Requests\InboxSftpRequest;
  9  use App\Http\Services\AutoRequirementService;
 10  use App\Http\Services\CertificationRequestService;
 11  use App\Http\Services\EtrService;
 12  use App\Http\Services\GpgService;
 13  use App\Http\Services\InboxFileService;
 14  use App\Http\Services\JsonImporterService;
 15  use App\Http\Services\PolService;
 16  use App\Http\Services\TaskService;
 17  use App\Models\Dossier;
 18  use App\Models\InboxFile;
 19  use App\Models\Stage;
 20  use App\Models\User;
 21  use App\Notifications\FileUploaded;
 22  use App\Notifications\ORUploadedNotification;
 23  use App\Notifications\OtherUploaded;
 24  use Exception;
 25  use Illuminate\Http\JsonResponse;
 26  use Illuminate\Support\Facades\DB;
 27  use Illuminate\Support\Facades\Storage;
 28  use Illuminate\Support\Str;
 29  use Spatie\TemporaryDirectory\TemporaryDirectory;
 30  
 31  class InboxSftpFileForDownload extends Controller
 32  {
 33      public string $path;
 34      public string $filename;
 35      public ?User $user;
 36      public ?Dossier $dossier;
 37      public ?array $errors = null;
 38  
 39      /**
 40       * Handle the incoming request.
 41       *
 42       * @return JsonResponse response()->json(['errors' => ['errors' => ['code' => 'ERROR MSG']]], 400);
 43       * @throws Exception
 44       */
 45      public function __invoke(InboxSftpRequest $request)
 46      {
 47          $this->jsonImport($request->get('path'));
 48  
 49          try {
 50              $validated = $request->validated();
 51  
 52              $tmpDir = (new TemporaryDirectory())
 53                  ->location(Storage::path('tmp'))
 54                  ->deleteWhenDestroyed()
 55                  ->create();
 56  
 57              $tmpPath = $tmpDir->path($validated['filename']);
 58              $this->path = $this->saveFile($validated['path'], $tmpPath);
 59              $this->filename = basename($validated['filename']);
 60              $this->user = $validated['user'];
 61              $this->dossier = $validated['dossier'];
 62  
 63              $inboxFileSaver = new InboxFileSaver(
 64                  $this->path,
 65                  $this->filename,
 66                  $this->user->id,
 67                  $this->dossier->id ?? null
 68              );
 69  
 70              $type = $inboxFileSaver->type; // Type extracted from file name
 71  
 72              if ($type === null) {
 73                  return response()->json(['errors' => ['errors' => ['code' => __('files.notifications.not_valid.message')]]], 400);
 74              }
 75  
 76              if (
 77                  !$this->dossier and
 78                  $type !== InboxFileTypeEnum::certification_request() and
 79                  $type !== InboxFileTypeEnum::others()
 80              ) {
 81                  return $this->arrayToErrorResponse(['Code must reference an existing dossier']);
 82              }
 83  
 84              $inboxFileSaver->decrypt();
 85  
 86              $errors = match ($type) {
 87                  InboxFileTypeEnum::certification_request() => $this->handleCertificationRequest($inboxFileSaver),
 88                  InboxFileTypeEnum::evaluation_request() => $this->handleEvaluationRequest($inboxFileSaver),
 89                  InboxFileTypeEnum::partial_report() => $this->handlePartialReport($inboxFileSaver),
 90                  InboxFileTypeEnum::ETR() => $this->handleETR($inboxFileSaver),
 91                  InboxFileTypeEnum::OR() => $this->handleOR($inboxFileSaver),
 92                  default => $this->handleOthers($inboxFileSaver),
 93              };
 94  
 95              if (!empty($errors)) {
 96                  return $this->arrayToErrorResponse($errors);
 97              }
 98          } catch (\Exception $e) {
 99              logger()->error($e);
100              return response()->json(['errors' => ['errors' => ['code' => $e->getMessage()]]], 400);
101          }
102  
103          return response()->json(['message' => __('files.notifications.success.message')], 200);
104      }
105  
106      private function saveFile(string $sftpPath, string $tmpPath)
107      {
108          $remoteStream = Storage::disk('sftp')->readStream($sftpPath);
109          if (!$remoteStream) {
110              throw new \Exception("Could not open remote sftp::{$sftpPath}");
111          }
112          $localStream = fopen($tmpPath, 'w');
113          if (!$localStream) {
114              throw new \Exception("Could not open local local::{$tmpPath}");
115          }
116          stream_copy_to_stream($remoteStream, $localStream);
117          fclose($remoteStream);
118          fclose($localStream);
119  
120          Storage::disk('sftp')->delete($sftpPath);
121  
122          return Str::after($tmpPath, storage_path('app') . '/');
123      }
124  
125      private function arrayToErrorResponse(array $errors): JsonResponse
126      {
127          return response()->json(['errors' => $errors], 400);
128      }
129  
130      private function subsanateDossier(InboxFileSaver $saver)
131      {
132          $inboxfile = $this->saveInboxFile($saver);
133          $inboxfile->fresh();
134  
135          $this->dossier
136              ->getNotifiableUser()
137              ->notify(new FileUploaded($this->dossier, $inboxfile->document->getFolderPath([1, 2])));
138      }
139  
140      private function handleCertificationRequest(InboxFileSaver $saver)
141      {
142          // If dossier exists we take it as a subsanation
143          if ($this->dossier) {
144              $this->subsanateDossier($saver);
145              return [];
146          }
147          try {
148              $certificationRequest = new CertificationRequestService($saver->path, $this->user->id);
149              $this->errors = $certificationRequest->readAndValidate();
150          } catch (\Exception $e) {
151              logger()->error('Error while handling certification request', [
152                  'message' => $e->getMessage(),
153                  'trace' => $e->getTraceAsString(),
154              ]);
155              return ['Ha habido un error al procesar la solicitud de certificación'];
156          }
157  
158          try {
159              DB::beginTransaction();
160  
161              // We will try to create the dossier even if there are errors in
162              // the certification request
163              $this->dossier = $this->createDossierFromInboxFile($saver, $certificationRequest);
164  
165              // If the norm is not CC or LINCE, we save the document without commiting.
166              // Else, we need to check the FOR-001 and POL structure
167              $normGroupName = $this->dossier->norm->normGroup->name ?? null;
168              if (!in_array($normGroupName, ['Common Criteria', 'LINCE'])) {
169                  $this->handleOthers($saver);
170                  return [];
171              }
172  
173              // Check POL
174              $service = new PolService($normGroupName, 'certification_request');
175              $inboxFile = $this->dossier->inboxFiles()->first();
176              if (!$service->verifyDirectoryStructure($inboxFile->path)) {
177                  return [__('file.handlers.errors.directory_structure.message')];
178              }
179  
180              if ($this->errors) {
181                  return $this->errors;
182              }
183  
184              DB::commit();
185          } catch (\Exception $e) {
186              DB::rollBack();
187              logger()->error('Error while handling certification request', [
188                  'message' => $e->getMessage(),
189                  'trace' => $e->getTraceAsString(),
190              ]);
191  
192              $this->errors = []; // Discard errors and try a different method
193              $this->errors = $this->handleOthers($saver);
194              DB::commit();
195          }
196  
197          return $this->errors;
198      }
199  
200      private function handleEvaluationRequest(InboxFileSaver $saver)
201      {
202          $errors = $this->checkPol($saver);
203          if (count($errors) > 0) {
204              return $errors;
205          }
206  
207          $inboxFile = $this->saveInboxFile($saver);
208          $inboxFile->is_validated = true;
209          $inboxFile->save();
210  
211          // create tasks
212          InboxFileService::evaluationRequestReceived($saver);
213  
214          return [];
215      }
216  
217      private function handlePartialReport(InboxFileSaver $saver)
218      {
219          if ($this->dossier->monitoring_level === DossierLevelEnum::basic()->value)
220              return [__('file.handlers.errors.partial_report.message')];
221  
222          $inboxFile = $this->saveInboxFile($saver);
223  
224          // Create tasks
225          InboxFileService::partialReportReceived($saver, $inboxFile);
226          return [];
227      }
228  
229      private function handleETR(InboxFileSaver $saver)
230      {
231          $errors = $this->checkETRAcceptance();
232          if (count($errors) > 0) {
233              return $errors;
234          }
235  
236          $errors = $this->checkPol($saver);
237          if (count($errors) > 0) {
238              return $errors;
239          }
240  
241          $inboxFile = $this->saveInboxFile($saver);
242  
243          // Create tasks
244          InboxFileService::ETRReceived($saver, $inboxFile);
245  
246          return [];
247      }
248  
249      private function handleOR(InboxFileSaver $saver)
250      {
251          $inboxFile = $this->saveInboxFile($saver);
252          $inboxFile->is_validated = true;
253          $inboxFile->save();
254  
255          $saver->dossier->getNotifiableUser()->notify(new ORUploadedNotification($saver->dossier));
256          return [];
257      }
258  
259      /*
260      * Handle files that are not certification requests, evaluation requests, partial reports, ETRs or ORs
261      */
262      private function handleOthers(InboxFileSaver $saver)
263      {
264          $dossier = $saver->dossier;
265  
266          $inboxFile = $this->saveInboxFile($saver);
267          $inboxFile->is_validated = false;
268          $inboxFile->save();
269  
270          $user = $dossier ? $dossier->getNotifiableUser() : User::technicalManager();
271          $user->notify(new OtherUploaded($saver->dossier->code ?? null, $this->filename));
272  
273          return [];
274      }
275  
276      private function checkPol(InboxFileSaver $saver)
277      {
278          $normName = $this->dossier ? $this->dossier->norm->normGroup->name : $saver->dossier->norm->normGroup->name;
279  
280          $service = new PolService($normName, $saver->type->label);
281          if (!$service->verifyDirectoryStructure($saver->path)) {
282              return [__('file.handlers.errors.directory_structure.message')];
283          }
284  
285          return [];
286      }
287  
288      private function createDossierFromInboxFile(
289          InboxFileSaver              $saver,
290          CertificationRequestService $certificationRequest
291      ): Dossier {
292          $this->dossier = $certificationRequest->build(false);
293          $inboxFile = $this->saveInboxFile($saver);
294          TaskService::createNewDossierTask($this->dossier, $inboxFile->reviewer_id);
295  
296          return $this->dossier;
297      }
298  
299      private function saveInboxFile(InboxFileSaver $saver): InboxFile
300      {
301          $inboxFile = $saver->save($this->dossier->id ?? null);
302          if ($inboxFile->validation) {
303              AutoRequirementService::run($inboxFile->validation);
304          }
305  
306          return $inboxFile;
307      }
308  
309      /**
310       * @throws Exception
311       */
312      private function jsonImport(string $path): void
313      {
314          /*  Every zip file, no matter what name format it has, even if it triggers other methods.
315              It must be searched for json files */
316  
317          if (Str::endsWith($path, '.zip.pgp')) {
318              $gpgService = new GpgService();
319              $zipPath = (new TemporaryDirectory())
320                  ->create()
321                  ->deleteWhenDestroyed()
322                  ->path('inboxFile.zip');
323  
324              Storage::put($zipPath, $gpgService->decryptAndVerify(Storage::disk('sftp')->get($path)));
325              JsonImporterService::import($zipPath);
326          }
327      }
328  
329      private function checkETRAcceptance()
330      {
331          if (DossierLevelEnum::basic()->value === $this->dossier->monitoring_level) {
332              return [];
333          }
334  
335          $missingEtrps = EtrService::checkETRps($this->dossier);
336          if ($missingEtrps) {
337              return [__('file.handlers.errors.missing_partial_reports.message', ['missingEtrps' => implode(', ', $missingEtrps)])];
338          }
339  
340          return [];
341      }
342  }