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 }