|
16 | 16 | use App\Helpers\ExercisesConfig; |
17 | 17 | use App\Helpers\FileStorageManager; |
18 | 18 | use App\Model\Entity\Assignment; |
| 19 | +use App\Model\Entity\AttachmentFile; |
| 20 | +use App\Model\Entity\Exercise; |
19 | 21 | use App\Model\Entity\ExerciseFile; |
| 22 | +use App\Model\Entity\ExerciseFileLink; |
20 | 23 | use App\Model\Entity\UploadedFile; |
21 | | -use App\Model\Entity\AttachmentFile; |
22 | 24 | use App\Model\Repository\Assignments; |
23 | 25 | use App\Model\Repository\AttachmentFiles; |
24 | 26 | use App\Model\Repository\Exercises; |
25 | | -use App\Model\Entity\Exercise; |
| 27 | +use App\Model\Repository\ExerciseFileLinks; |
26 | 28 | use App\Model\Repository\ExerciseFiles; |
27 | 29 | use App\Model\Repository\UploadedFiles; |
| 30 | +use App\Security\Roles; |
28 | 31 | use App\Security\ACL\IExercisePermissions; |
29 | 32 | use Exception; |
30 | 33 |
|
@@ -58,6 +61,12 @@ class ExerciseFilesPresenter extends BasePresenter |
58 | 61 | */ |
59 | 62 | public $exerciseFiles; |
60 | 63 |
|
| 64 | + /** |
| 65 | + * @var ExerciseFileLinks |
| 66 | + * @inject |
| 67 | + */ |
| 68 | + public $fileLinks; |
| 69 | + |
61 | 70 | /** |
62 | 71 | * @var AttachmentFiles |
63 | 72 | * @inject |
@@ -88,6 +97,12 @@ class ExerciseFilesPresenter extends BasePresenter |
88 | 97 | */ |
89 | 98 | public $configChecker; |
90 | 99 |
|
| 100 | + /** |
| 101 | + * @var Roles |
| 102 | + * @inject |
| 103 | + */ |
| 104 | + public $roles; |
| 105 | + |
91 | 106 | public function checkUploadExerciseFiles(string $id) |
92 | 107 | { |
93 | 108 | $exercise = $this->exercises->findOrThrow($id); |
@@ -352,7 +367,7 @@ public function checkDeleteAttachmentFile(string $id, string $fileId) |
352 | 367 | * @throws NotFoundException |
353 | 368 | */ |
354 | 369 | #[Path("id", new VUuid(), "identification of exercise", required: true)] |
355 | | - #[Path("fileId", new VString(), "identification of file", required: true)] |
| 370 | + #[Path("fileId", new VUuid(), "identification of file", required: true)] |
356 | 371 | public function actionDeleteAttachmentFile(string $id, string $fileId) |
357 | 372 | { |
358 | 373 | $exercise = $this->exercises->findOrThrow($id); |
@@ -428,4 +443,182 @@ public function actionDownloadAttachmentFilesArchive(string $id) |
428 | 443 | } |
429 | 444 | $this->sendZipFilesResponse($files, "exercise-attachment-{$id}.zip"); |
430 | 445 | } |
| 446 | + |
| 447 | + /* |
| 448 | + * Exercise file links management |
| 449 | + */ |
| 450 | + |
| 451 | + public function checkGetFileLinks(string $id) |
| 452 | + { |
| 453 | + $exercise = $this->exercises->findOrThrow($id); |
| 454 | + if (!$this->exerciseAcl->canUpdate($exercise)) { |
| 455 | + throw new ForbiddenRequestException("You cannot view exercise file links for this exercise."); |
| 456 | + } |
| 457 | + } |
| 458 | + |
| 459 | + /** |
| 460 | + * Retrieve a list of all exercise-file links for given exercise. |
| 461 | + * @GET |
| 462 | + */ |
| 463 | + #[Path("id", new VUuid(), "of exercise", required: true)] |
| 464 | + public function actionGetFileLinks(string $id) |
| 465 | + { |
| 466 | + $exercise = $this->exercises->findOrThrow($id); |
| 467 | + $this->sendSuccessResponse($exercise->getFileLinks()->getValues()); |
| 468 | + } |
| 469 | + |
| 470 | + public function checkCreateFileLink(string $id) |
| 471 | + { |
| 472 | + $exercise = $this->exercises->findOrThrow($id); |
| 473 | + if (!$this->exerciseAcl->canUpdate($exercise)) { |
| 474 | + throw new ForbiddenRequestException("You cannot create exercise file links for this exercise."); |
| 475 | + } |
| 476 | + } |
| 477 | + /** |
| 478 | + * Create a new exercise-file link for given exercise. |
| 479 | + * @POST |
| 480 | + */ |
| 481 | + #[Path("id", new VUuid(), "of exercise", required: true)] |
| 482 | + #[Post( |
| 483 | + "exerciseFileId", |
| 484 | + new VUuid(), |
| 485 | + "Target file the link will point to", |
| 486 | + required: true |
| 487 | + )] |
| 488 | + #[Post( |
| 489 | + "key", |
| 490 | + new VString(1, 16), |
| 491 | + "Internal user-selected identifier of the exercise file link within the exercise", |
| 492 | + required: true |
| 493 | + )] |
| 494 | + #[Post( |
| 495 | + "requiredRole", |
| 496 | + new VString(1, 255), |
| 497 | + "Minimal required user role to access the file (null = non-logged-in users)", |
| 498 | + nullable: true, |
| 499 | + required: false |
| 500 | + )] |
| 501 | + #[Post( |
| 502 | + "saveName", |
| 503 | + new VString(1, 255), |
| 504 | + "File name override (the file will be downloaded under this name instead of the original name)", |
| 505 | + nullable: true, |
| 506 | + required: false |
| 507 | + )] |
| 508 | + public function actionCreateFileLink(string $id) |
| 509 | + { |
| 510 | + $exercise = $this->exercises->findOrThrow($id); |
| 511 | + $req = $this->getRequest(); |
| 512 | + $exerciseFile = $this->exerciseFiles->findOrThrow($req->getPost("exerciseFileId")); |
| 513 | + $key = $req->getPost("key"); |
| 514 | + $requiredRole = $req->getPost("requiredRole"); |
| 515 | + $saveName = $req->getPost("saveName"); |
| 516 | + |
| 517 | + if (!$this->roles->validateRole($requiredRole)) { |
| 518 | + throw new InvalidApiArgumentException('requiredRole', "Unknown user role '$requiredRole'"); |
| 519 | + } |
| 520 | + |
| 521 | + $link = ExerciseFileLink::createForExercise( |
| 522 | + $key, |
| 523 | + $exerciseFile, |
| 524 | + $exercise, |
| 525 | + $requiredRole, |
| 526 | + $saveName |
| 527 | + ); |
| 528 | + |
| 529 | + $this->fileLinks->persist($link); |
| 530 | + $this->sendSuccessResponse($link); |
| 531 | + } |
| 532 | + |
| 533 | + public function checkUpdateFileLink(string $id, string $linkId) |
| 534 | + { |
| 535 | + $exercise = $this->exercises->findOrThrow($id); |
| 536 | + $link = $this->fileLinks->findOrThrow($linkId); |
| 537 | + |
| 538 | + if ($link->getExercise()?->getId() !== $id) { |
| 539 | + throw new BadRequestException("The exercise file link is not associated with the given exercise."); |
| 540 | + } |
| 541 | + |
| 542 | + if (!$this->exerciseAcl->canUpdate($exercise)) { |
| 543 | + throw new ForbiddenRequestException("You cannot update exercise file links for this exercise."); |
| 544 | + } |
| 545 | + } |
| 546 | + /** |
| 547 | + * Update a specific exercise-file link. |
| 548 | + * @POST |
| 549 | + */ |
| 550 | + #[Path("id", new VUuid(), "of exercise", required: true)] |
| 551 | + #[Path("linkId", new VUuid(), "of the exercise file link", required: true)] |
| 552 | + #[Post( |
| 553 | + "key", |
| 554 | + new VString(1, 16), |
| 555 | + "Internal user-selected identifier of the exercise file link within the exercise", |
| 556 | + required: true |
| 557 | + )] |
| 558 | + #[Post( |
| 559 | + "requiredRole", |
| 560 | + new VString(1, 255), |
| 561 | + "Minimal required user role to access the file (null = non-logged-in users)", |
| 562 | + nullable: true, |
| 563 | + required: false |
| 564 | + )] |
| 565 | + #[Post( |
| 566 | + "saveName", |
| 567 | + new VString(1, 255), |
| 568 | + "File name override (the file will be downloaded under this name instead of the original name)", |
| 569 | + nullable: true, |
| 570 | + required: false |
| 571 | + )] |
| 572 | + public function actionUpdateFileLink(string $id, string $linkId) |
| 573 | + { |
| 574 | + $link = $this->fileLinks->findOrThrow($linkId); |
| 575 | + $req = $this->getRequest(); |
| 576 | + $post = $req->getPost(); |
| 577 | + |
| 578 | + if (array_key_exists("requiredRole", $post)) { |
| 579 | + // array_key_exists checks whether the key is present (even if null) |
| 580 | + $requiredRole = $post["requiredRole"]; |
| 581 | + if (!$this->roles->validateRole($requiredRole)) { |
| 582 | + throw new InvalidApiArgumentException('requiredRole', "Unknown user role '$requiredRole'"); |
| 583 | + } |
| 584 | + $link->setRequiredRole($requiredRole); |
| 585 | + } |
| 586 | + |
| 587 | + if (array_key_exists("saveName", $post)) { |
| 588 | + // array_key_exists checks whether the key is present (even if null) |
| 589 | + $link->setSaveName($post["saveName"]); |
| 590 | + } |
| 591 | + |
| 592 | + $link->setKey($req->getPost("key")); |
| 593 | + |
| 594 | + $this->fileLinks->persist($link); |
| 595 | + $this->sendSuccessResponse($link); |
| 596 | + } |
| 597 | + |
| 598 | + public function checkDeleteFileLink(string $id, string $linkId) |
| 599 | + { |
| 600 | + $exercise = $this->exercises->findOrThrow($id); |
| 601 | + $link = $this->fileLinks->findOrThrow($linkId); |
| 602 | + |
| 603 | + if ($link->getExercise()?->getId() !== $id) { |
| 604 | + throw new BadRequestException("The exercise file link is not associated with the given exercise."); |
| 605 | + } |
| 606 | + |
| 607 | + if (!$this->exerciseAcl->canUpdate($exercise)) { |
| 608 | + throw new ForbiddenRequestException("You cannot delete exercise file links for this exercise."); |
| 609 | + } |
| 610 | + } |
| 611 | + |
| 612 | + /** |
| 613 | + * Delete a specific exercise-file link. |
| 614 | + * @DELETE |
| 615 | + */ |
| 616 | + #[Path("id", new VUuid(), "of exercise", required: true)] |
| 617 | + #[Path("linkId", new VUuid(), "of the exercise file link", required: true)] |
| 618 | + public function actionDeleteFileLink(string $id, string $linkId) |
| 619 | + { |
| 620 | + $link = $this->fileLinks->findOrThrow($linkId); |
| 621 | + $this->fileLinks->remove($link); |
| 622 | + $this->sendSuccessResponse("OK"); |
| 623 | + } |
431 | 624 | } |
0 commit comments