diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index ef9f86578..0b801c727 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -11,32 +11,6 @@ password]]> - - - action]]> - action]]> - audioEmbed]]> - audioEmbed]]> - audioHref]]> - audioHref]]> - date]]> - date]]> - description]]> - description]]> - href]]> - href]]> - sourceHref]]> - sourceHref]]> - sourceName]]> - sourceName]]> - title]]> - title]]> - videoEmbed]]> - videoEmbed]]> - videoHref]]> - videoHref]]> - - lead === '' ? null : $values->lead]]> @@ -61,52 +35,6 @@ site->new->url]]> - - - action]]> - action]]> - date]]> - date]]> - description]]> - description]]> - event]]> - event]]> - eventHref]]> - eventHref]]> - favorite]]> - favorite]]> - filenamesTalk]]> - filenamesTalk]]> - href]]> - href]]> - locale]]> - locale]]> - ogImage]]> - ogImage]]> - publishSlides]]> - publishSlides]]> - slidesEmbed]]> - slidesEmbed]]> - slidesHref]]> - slidesHref]]> - slidesNote]]> - slidesNote]]> - slidesTalk]]> - slidesTalk]]> - supersededBy]]> - supersededBy]]> - title]]> - title]]> - transcript]]> - transcript]]> - translationGroup]]> - translationGroup]]> - videoEmbed]]> - videoEmbed]]> - videoHref]]> - videoHref]]> - - @@ -210,12 +138,6 @@ - - - videoThumbnail]]> - videoThumbnailAlternative]]> - - diff --git a/app/src/Form/InterviewFormFactory.php b/app/src/Form/InterviewFormFactory.php index 370895ac9..80eb5e00c 100644 --- a/app/src/Form/InterviewFormFactory.php +++ b/app/src/Form/InterviewFormFactory.php @@ -9,6 +9,7 @@ use MichalSpacekCz\Media\VideoThumbnails; use Nette\Forms\Controls\SubmitButton; use Nette\Forms\Form; +use Nette\Http\FileUpload; readonly class InterviewFormFactory { @@ -71,8 +72,21 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor $form->onSuccess[] = function (UiForm $form) use ($interview, $onSuccess, $videoThumbnailFormFields): void { $values = $form->getFormValues(); - $videoThumbnailBasename = $this->videoThumbnails->getUploadedMainFileBasename($values); - $videoThumbnailBasenameAlternative = $this->videoThumbnails->getUploadedAlternativeFileBasename($values); + assert($values->videoThumbnail instanceof FileUpload); + assert($values->videoThumbnailAlternative instanceof FileUpload); + assert(is_string($values->action)); + assert(is_string($values->title)); + assert(is_string($values->description)); + assert(is_string($values->date)); + assert(is_string($values->href)); + assert(is_string($values->audioHref)); + assert(is_string($values->audioEmbed)); + assert(is_string($values->videoHref)); + assert(is_string($values->videoEmbed)); + assert(is_string($values->sourceName)); + assert(is_string($values->sourceHref)); + $videoThumbnailBasename = $this->videoThumbnails->getUploadedMainFileBasename($values->videoThumbnail); + $videoThumbnailBasenameAlternative = $this->videoThumbnails->getUploadedAlternativeFileBasename($values->videoThumbnailAlternative); if ($interview) { $removeVideoThumbnail = $videoThumbnailFormFields->hasVideoThumbnail() && $values->removeVideoThumbnail; $removeVideoThumbnailAlternative = $videoThumbnailFormFields->hasAlternativeVideoThumbnail() && $values->removeVideoThumbnailAlternative; @@ -94,7 +108,7 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor $values->sourceName, $values->sourceHref, ); - $this->videoThumbnails->saveVideoThumbnailFiles($interview->getId(), $values); + $this->videoThumbnails->saveVideoThumbnailFiles($interview->getId(), $values->videoThumbnail, $values->videoThumbnailAlternative); if ($removeVideoThumbnail && $thumbnailFilename !== null) { $this->videoThumbnails->deleteFile($interview->getId(), $thumbnailFilename); } @@ -117,7 +131,7 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor $values->sourceName, $values->sourceHref, ); - $this->videoThumbnails->saveVideoThumbnailFiles($interviewId, $values); + $this->videoThumbnails->saveVideoThumbnailFiles($interviewId, $values->videoThumbnail, $values->videoThumbnailAlternative); } $onSuccess(); }; diff --git a/app/src/Form/SignInHoneypotFormFactory.php b/app/src/Form/SignInHoneypotFormFactory.php index e0d38a8d8..766ee20ea 100644 --- a/app/src/Form/SignInHoneypotFormFactory.php +++ b/app/src/Form/SignInHoneypotFormFactory.php @@ -26,6 +26,8 @@ public function create(): UiForm $this->controlsFactory->addSignIn($form); $form->onSuccess[] = function (UiForm $form): void { $values = $form->getFormValues(); + assert(is_string($values->username)); + assert(is_string($values->password)); Debugger::log("Sign-in attempt: {$values->username}, {$values->password}, {$this->httpRequest->getRemoteAddress()}", 'honeypot'); $creds = $values->username . ':' . $values->password; if (Regex::isMatch('~\slimit\s~i', $creds)) { diff --git a/app/src/Form/TalkFormFactory.php b/app/src/Form/TalkFormFactory.php index e1cfd53ba..f7cf45dea 100644 --- a/app/src/Form/TalkFormFactory.php +++ b/app/src/Form/TalkFormFactory.php @@ -11,6 +11,7 @@ use MichalSpacekCz\Talks\Talks; use Nette\Forms\Controls\SubmitButton; use Nette\Forms\Form; +use Nette\Http\FileUpload; use Nette\Utils\Html; use Nette\Utils\Strings; @@ -107,8 +108,32 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm $form->onSuccess[] = function (UiForm $form) use ($talk, $onSuccess, $videoThumbnailFormFields): void { $values = $form->getFormValues(); - $videoThumbnailBasename = $this->videoThumbnails->getUploadedMainFileBasename($values); - $videoThumbnailBasenameAlternative = $this->videoThumbnails->getUploadedAlternativeFileBasename($values); + assert($values->videoThumbnail instanceof FileUpload); + assert($values->videoThumbnailAlternative instanceof FileUpload); + assert(is_int($values->locale)); + assert(is_int($values->translationGroup) || $values->translationGroup === null); + assert(is_string($values->action)); + assert(is_string($values->title)); + assert(is_string($values->description)); + assert(is_string($values->date)); + assert(is_string($values->duration)); + assert(is_string($values->href)); + assert(is_int($values->slidesTalk) || $values->slidesTalk === null); + assert(is_int($values->filenamesTalk) || $values->filenamesTalk === null); + assert(is_string($values->slidesHref)); + assert(is_string($values->slidesEmbed)); + assert(is_string($values->slidesNote)); + assert(is_string($values->videoHref)); + assert(is_string($values->videoEmbed)); + assert(is_string($values->event)); + assert(is_string($values->eventHref)); + assert(is_string($values->ogImage)); + assert(is_string($values->transcript)); + assert(is_string($values->favorite)); + assert(is_int($values->supersededBy) || $values->supersededBy === null); + assert(is_bool($values->publishSlides)); + $videoThumbnailBasename = $this->videoThumbnails->getUploadedMainFileBasename($values->videoThumbnail); + $videoThumbnailBasenameAlternative = $this->videoThumbnails->getUploadedAlternativeFileBasename($values->videoThumbnailAlternative); if ($talk) { $removeVideoThumbnail = $videoThumbnailFormFields->hasVideoThumbnail() && $values->removeVideoThumbnail; $removeVideoThumbnailAlternative = $videoThumbnailFormFields->hasAlternativeVideoThumbnail() && $values->removeVideoThumbnailAlternative; @@ -141,7 +166,7 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm $values->supersededBy, $values->publishSlides, ); - $this->videoThumbnails->saveVideoThumbnailFiles($talk->getId(), $values); + $this->videoThumbnails->saveVideoThumbnailFiles($talk->getId(), $values->videoThumbnail, $values->videoThumbnailAlternative); if ($removeVideoThumbnail && $thumbnailFilename !== null) { $this->videoThumbnails->deleteFile($talk->getId(), $thumbnailFilename); } @@ -176,7 +201,7 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm $values->supersededBy, $values->publishSlides, ); - $this->videoThumbnails->saveVideoThumbnailFiles($talkId, $values); + $this->videoThumbnails->saveVideoThumbnailFiles($talkId, $values->videoThumbnail, $values->videoThumbnailAlternative); $message = Html::el()->setText('Přednáška přidána '); } $message->addHtml(Html::el('a')->href($this->linkGenerator->link('Www:Talks:talk', [$values->action]))->setText('Zobrazit')); diff --git a/app/src/Form/TalkSlidesFormFactory.php b/app/src/Form/TalkSlidesFormFactory.php index 12bb0e9d7..865075b36 100644 --- a/app/src/Form/TalkSlidesFormFactory.php +++ b/app/src/Form/TalkSlidesFormFactory.php @@ -65,7 +65,7 @@ public function create(callable $onSuccess, int $talkId, TalkSlideCollection $sl assert($values->slides instanceof ArrayHash); assert($values->new instanceof ArrayHash); assert(is_bool($values->deleteReplaced)); - $this->talkSlides->saveSlides($talkId, $slides, (array)$values->slides, array_values((array)$values->new), $values->deleteReplaced); + $this->talkSlides->saveSlides($talkId, $slides, $values->slides, $values->new, $values->deleteReplaced); $message = $this->texyFormatter->translate('messages.talks.admin.slideadded'); $type = 'info'; } catch (DuplicatedSlideException $e) { diff --git a/app/src/Media/VideoThumbnails.php b/app/src/Media/VideoThumbnails.php index c01237e1d..a11f8bf22 100644 --- a/app/src/Media/VideoThumbnails.php +++ b/app/src/Media/VideoThumbnails.php @@ -13,7 +13,6 @@ use Nette\Http\FileUpload; use Nette\Utils\Callback; use Nette\Utils\ImageException; -use stdClass; readonly class VideoThumbnails { @@ -114,18 +113,18 @@ public function deleteFile(int $id, string $basename): void /** * @throws ContentTypeException */ - public function getUploadedMainFileBasename(stdClass $values): ?string + public function getUploadedMainFileBasename(FileUpload $thumbnail): ?string { - return $this->getUploadedFileBasename($values->videoThumbnail, $this->supportedImageFileFormats->getMainExtensionByContentType(...)); + return $this->getUploadedFileBasename($thumbnail, $this->supportedImageFileFormats->getMainExtensionByContentType(...)); } /** * @throws ContentTypeException */ - public function getUploadedAlternativeFileBasename(stdClass $values): ?string + public function getUploadedAlternativeFileBasename(FileUpload $thumbnail): ?string { - return $this->getUploadedFileBasename($values->videoThumbnailAlternative, $this->supportedImageFileFormats->getAlternativeExtensionByContentType(...)); + return $this->getUploadedFileBasename($thumbnail, $this->supportedImageFileFormats->getAlternativeExtensionByContentType(...)); } @@ -149,15 +148,15 @@ private function getUploadedFileBasename(FileUpload $thumbnail, callable $getExt /** * @throws ContentTypeException */ - public function saveVideoThumbnailFiles(int $id, stdClass $values): void + public function saveVideoThumbnailFiles(int $id, FileUpload $videoThumbnail, FileUpload $videoThumbnailAlternative): void { - $basename = $this->getUploadedMainFileBasename($values); + $basename = $this->getUploadedMainFileBasename($videoThumbnail); if ($basename !== null) { - $values->videoThumbnail->move($this->mediaResources->getImageFilename($id, $basename)); + $videoThumbnail->move($this->mediaResources->getImageFilename($id, $basename)); } - $basename = $this->getUploadedAlternativeFileBasename($values); + $basename = $this->getUploadedAlternativeFileBasename($videoThumbnailAlternative); if ($basename !== null) { - $values->videoThumbnailAlternative->move($this->mediaResources->getImageFilename($id, $basename)); + $videoThumbnailAlternative->move($this->mediaResources->getImageFilename($id, $basename)); } } diff --git a/app/src/Talks/Slides/TalkSlides.php b/app/src/Talks/Slides/TalkSlides.php index 5d90d4f6d..1c5dea6fe 100644 --- a/app/src/Talks/Slides/TalkSlides.php +++ b/app/src/Talks/Slides/TalkSlides.php @@ -204,19 +204,16 @@ private function replaceSlideImage(int $talkId, FileUpload $replace, callable $g /** - * Insert slides. - * - * @param int $talkId - * @param list> $slides * @throws DuplicatedSlideException * @throws ContentTypeException * @throws SlideImageUploadFailedException */ - private function addSlides(int $talkId, array $slides): void + private function addSlides(int $talkId, ArrayHash $slides): void { $lastNumber = 0; try { foreach ($slides as $slide) { + assert($slide instanceof ArrayHash); assert($slide->replace instanceof FileUpload); assert($slide->replaceAlternative instanceof FileUpload); assert(is_int($slide->number)); @@ -245,16 +242,13 @@ private function addSlides(int $talkId, array $slides): void /** - * Update slides. - * - * @param array> $slides * @param bool $removeFiles Remove old files? * @throws DuplicatedSlideException * @throws ContentTypeException * @throws SlideImageUploadFailedException * @throws TalkSlideDoesNotExistException */ - private function updateSlides(int $talkId, TalkSlideCollection $originalSlides, array $slides, bool $removeFiles): void + private function updateSlides(int $talkId, TalkSlideCollection $originalSlides, ArrayHash $slides, bool $removeFiles): void { foreach ($originalSlides as $slide) { foreach ($slide->getAllFilenames() as $filename) { @@ -262,6 +256,7 @@ private function updateSlides(int $talkId, TalkSlideCollection $originalSlides, } } foreach ($slides as $id => $slide) { + assert($slide instanceof ArrayHash); assert($slide->replace instanceof FileUpload || $slide->replace === null); assert($slide->replaceAlternative instanceof FileUpload || $slide->replaceAlternative === null); assert(is_string($slide->alias)); @@ -318,14 +313,12 @@ private function updateSlidesRow(int $talkId, string $alias, int $slideNumber, s /** - * @param array> $updateSlides - * @param list> $newSlides * @throws ContentTypeException * @throws DuplicatedSlideException * @throws SlideImageUploadFailedException * @throws TalkSlideDoesNotExistException */ - public function saveSlides(int $talkId, TalkSlideCollection $originalSlides, array $updateSlides, array $newSlides, bool $deleteReplaced): void + public function saveSlides(int $talkId, TalkSlideCollection $originalSlides, ArrayHash $updateSlides, ArrayHash $newSlides, bool $deleteReplaced): void { $this->otherSlides = []; $this->database->beginTransaction(); diff --git a/app/tests/Form/InterviewFormFactoryTest.phpt b/app/tests/Form/InterviewFormFactoryTest.phpt new file mode 100644 index 000000000..9e8e2c430 --- /dev/null +++ b/app/tests/Form/InterviewFormFactoryTest.phpt @@ -0,0 +1,67 @@ +formFactory->create( + function (): void { + $this->result = true; + }, + null, + ); + $form->setDefaults([ + 'action' => 'foo', + 'date' => '3210-09-08 10:20:30', + ]); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::true($this->result); + Assert::same([ + [ + 'action' => 'foo', + 'title' => '', + 'description' => null, + 'date' => '3210-09-08 10:20:30', + 'href' => '', + 'audio_href' => null, + 'audio_embed' => null, + 'video_href' => null, + 'video_thumbnail' => null, + 'video_thumbnail_alternative' => null, + 'video_embed' => null, + 'source_name' => '', + 'source_href' => '', + ], + ], $this->database->getParamsArrayForQuery('INSERT INTO interviews')); + } + +} + +TestCaseRunner::run(InterviewFormFactoryTest::class); diff --git a/app/tests/Form/TalkFormFactoryTest.phpt b/app/tests/Form/TalkFormFactoryTest.phpt new file mode 100644 index 000000000..142b00384 --- /dev/null +++ b/app/tests/Form/TalkFormFactoryTest.phpt @@ -0,0 +1,98 @@ +database->addFetchPairsResult([ + 123 => 'cs_CZ', + 321 => 'en_US', + ]); + } + + + #[Override] + protected function tearDown(): void + { + $this->database->reset(); + } + + + public function testCreateOnSuccessAdd(): void + { + $form = $this->formFactory->create( + function (Html $message): void { + $this->message = $message; + }, + null, + ); + $form->setDefaults([ + 'locale' => 123, + 'action' => 'foo', + 'date' => '3210-09-08 10:20:30', + ]); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same('Přednáška přidána Zobrazit', $this->message?->toHtml()); + Assert::same([ + [ + 'key_locale' => 123, + 'key_translation_group' => null, + 'action' => 'foo', + 'title' => '', + 'description' => null, + 'date' => '3210-09-08 10:20:30', + 'duration' => null, + 'href' => null, + 'key_talk_slides' => null, + 'key_talk_filenames' => null, + 'slides_href' => null, + 'slides_embed' => null, + 'slides_note' => null, + 'video_href' => null, + 'video_thumbnail' => null, + 'video_thumbnail_alternative' => null, + 'video_embed' => null, + 'event' => '', + 'event_href' => null, + 'og_image' => null, + 'transcript' => null, + 'favorite' => null, + 'key_superseded_by' => null, + 'publish_slides' => false, + ], + ], $this->database->getParamsArrayForQuery('INSERT INTO talks')); + } + +} + +TestCaseRunner::run(TalkFormFactoryTest::class); diff --git a/app/tests/Media/VideoThumbnailsTest.phpt b/app/tests/Media/VideoThumbnailsTest.phpt new file mode 100644 index 000000000..434b69159 --- /dev/null +++ b/app/tests/Media/VideoThumbnailsTest.phpt @@ -0,0 +1,73 @@ +videoThumbnails = new VideoThumbnails($mediaResources, $supportedImageFormats); + } + + + public function testGetUploadedMainFileBasename(): void + { + $upload = $this->getFileUpload('foo', UPLOAD_ERR_EXTENSION); + Assert::null($this->videoThumbnails->getUploadedMainFileBasename($upload)); + + $upload = $this->getFileUpload(__DIR__ . '/thumbnail-not-pic', UPLOAD_ERR_OK); + Assert::exception(function () use ($upload): void { + $this->videoThumbnails->getUploadedMainFileBasename($upload); + }, UnsupportedContentTypeException::class, 'Unsupported content type \'text/plain\', available types are {"image/gif":"gif","image/png":"png","image/jpeg":"jpg"}'); + + $upload = $this->getFileUpload(__DIR__ . '/thumbnail-gif-no-ext', UPLOAD_ERR_OK); + Assert::same('video-thumbnail.gif', $this->videoThumbnails->getUploadedMainFileBasename($upload)); + } + + + public function testGetUploadedAlternativeFileBasename(): void + { + $upload = $this->getFileUpload('foo', UPLOAD_ERR_EXTENSION); + Assert::null($this->videoThumbnails->getUploadedAlternativeFileBasename($upload)); + + $upload = $this->getFileUpload(__DIR__ . '/thumbnail-not-pic', UPLOAD_ERR_OK); + Assert::exception(function () use ($upload): void { + $this->videoThumbnails->getUploadedAlternativeFileBasename($upload); + }, UnsupportedContentTypeException::class, 'Unsupported content type \'text/plain\', available types are {"image/webp":"webp"}'); + + $upload = $this->getFileUpload(__DIR__ . '/thumbnail-webp-no-ext', UPLOAD_ERR_OK); + Assert::same('video-thumbnail.webp', $this->videoThumbnails->getUploadedAlternativeFileBasename($upload)); + } + + + private function getFileUpload(string $tmpName, int $error): FileUpload + { + return new FileUpload([ + 'name' => 'test', + 'size' => 123, + 'tmp_name' => $tmpName, + 'error' => $error, + ]); + } + +} + +TestCaseRunner::run(VideoThumbnailsTest::class); diff --git a/app/tests/Media/thumbnail-gif-no-ext b/app/tests/Media/thumbnail-gif-no-ext new file mode 100644 index 000000000..edaf2b97a Binary files /dev/null and b/app/tests/Media/thumbnail-gif-no-ext differ diff --git a/app/tests/Media/thumbnail-not-pic b/app/tests/Media/thumbnail-not-pic new file mode 100644 index 000000000..db0c8bc57 --- /dev/null +++ b/app/tests/Media/thumbnail-not-pic @@ -0,0 +1 @@ +This thumbnail is not a picture, which makes it not a thumbnail at all. Wait, what? diff --git a/app/tests/Media/thumbnail-webp-no-ext b/app/tests/Media/thumbnail-webp-no-ext new file mode 100644 index 000000000..2f8bf8fa1 Binary files /dev/null and b/app/tests/Media/thumbnail-webp-no-ext differ diff --git a/app/tests/Talks/Slides/TalkSlidesTest.phpt b/app/tests/Talks/Slides/TalkSlidesTest.phpt index 4608cb1f2..149219aba 100644 --- a/app/tests/Talks/Slides/TalkSlidesTest.phpt +++ b/app/tests/Talks/Slides/TalkSlidesTest.phpt @@ -52,14 +52,12 @@ class TalkSlidesTest extends TestCase $slides = new TalkSlideCollection(303); $slides->add(new TalkSlide(1, 'slide1', 1, 'slide1.jpg', 'slide-alt.jpg', null, 'Title 1', Html::fromText('Notes 1'), 'Notes 1', null, null, null)); $slides->add(new TalkSlide(2, 'slide2', 2, 'slide2.jpg', 'slide-alt.jpg', null, 'Title 2', Html::fromText('Notes 2'), 'Notes 2', null, null, null)); - $updateSlides = [ - 1 => $this->buildSlideArrayHash(1, 'foo1', 'Title 1', 'Speaker notes 1', 'slide1.jpg', null, 'slide-alt1.jpg', null), - 2 => $this->buildSlideArrayHash(2, 'foo2', 'Title 2', 'Speaker notes 2', 'slide2.jpg', null, 'slide-alt2.jpg', null), - ]; - $newSlides = [ - $this->buildSlideArrayHash(3, 'new1', 'New 1', 'New notes 1', null, new FileUpload(null), null, new FileUpload(null)), - $this->buildSlideArrayHash(4, 'new2', 'New 2', 'New notes 2', null, new FileUpload(null), null, new FileUpload(null)), - ]; + $updateSlides = new ArrayHash(); + $updateSlides[1] = $this->buildSlideArrayHash(1, 'foo1', 'Title 1', 'Speaker notes 1', 'slide1.jpg', null, 'slide-alt1.jpg', null); + $updateSlides[2] = $this->buildSlideArrayHash(2, 'foo2', 'Title 2', 'Speaker notes 2', 'slide2.jpg', null, 'slide-alt2.jpg', null); + $newSlides = new ArrayHash(); + $newSlides[0] = $this->buildSlideArrayHash(3, 'new1', 'New 1', 'New notes 1', null, new FileUpload(null), null, new FileUpload(null)); + $newSlides[1] = $this->buildSlideArrayHash(4, 'new2', 'New 2', 'New notes 2', null, new FileUpload(null), null, new FileUpload(null)); $this->talkSlides->saveSlides(303, $slides, $updateSlides, $newSlides, false); Assert::same(['slide1.jpg' => 0, 'slide-alt.jpg' => 1, 'slide2.jpg' => 0], PrivateProperty::getValue($this->talkSlides, 'otherSlides')); $paramsUpdate = $this->database->getParamsArrayForQuery('UPDATE talk_slides SET ? WHERE id_slide = ?');