From 0ae54b715c450376e69617af701fe280a786230b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 17 Nov 2024 05:50:37 +0100 Subject: [PATCH 01/24] Add TypedDatabase::fetchAll() and use it --- app/config/services.neon | 7 ++++--- app/psalm-baseline.xml | 7 ------- app/src/Articles/Articles.php | 6 +++--- app/src/Articles/Blog/BlogPostEdits.php | 6 +++--- app/src/Articles/Blog/BlogPostLocaleUrls.php | 6 +++--- app/src/Articles/Blog/BlogPosts.php | 4 +++- app/src/Database/TypedDatabase.php | 20 +++++++++++++++++++ app/src/Interviews/Interviews.php | 4 +++- app/src/Pulse/Companies.php | 4 +++- .../Algorithms/PasswordHashingAlgorithms.php | 2 +- .../PasswordHashingDisclosures.php | 2 +- app/src/Pulse/Passwords/Passwords.php | 8 ++++---- app/src/Pulse/Sites.php | 4 +++- app/src/Talks/Slides/TalkSlides.php | 4 ++-- app/src/Talks/Talks.php | 6 +++--- app/src/Tls/Certificates.php | 4 +++- .../TrainingApplicationStatusHistory.php | 4 +++- .../Applications/TrainingApplications.php | 4 ++-- app/src/Training/Company/CompanyTrainings.php | 4 +++- .../Training/Dates/TrainingDateStatuses.php | 6 +++--- app/src/Training/Dates/TrainingDates.php | 12 ++++++----- .../Training/Dates/UpcomingTrainingDates.php | 6 +++--- .../Discontinued/DiscontinuedTrainings.php | 8 ++++---- app/src/Training/Files/TrainingFiles.php | 4 ++-- .../Preliminary/PreliminaryTrainings.php | 8 ++++---- app/src/Training/Reviews/TrainingReviews.php | 8 +++++--- app/src/Training/Trainings/Trainings.php | 4 ++-- app/src/Training/Venues/TrainingVenues.php | 4 +++- app/src/Twitter/TwitterCards.php | 4 +++- app/src/UpcKeys/Technicolor.php | 7 ++++++- app/src/UpcKeys/Ubee.php | 10 ++++++---- 31 files changed, 115 insertions(+), 72 deletions(-) diff --git a/app/config/services.neon b/app/config/services.neon index 1a3d69063..9cdceeff3 100644 --- a/app/config/services.neon +++ b/app/config/services.neon @@ -31,6 +31,7 @@ services: create: MichalSpacekCz\Database\TypedDatabase autowired: MichalSpacekCz\Database\TypedDatabase typedDatabase.pulse: MichalSpacekCz\Database\TypedDatabase(@database.pulse.explorer) + typedDatabase.upcKeys: MichalSpacekCz\Database\TypedDatabase(@database.upcKeys.explorer) dateTimeFactory: MichalSpacekCz\DateTime\DateTimeFactory - MichalSpacekCz\DateTime\DateTimeFormatter(@translation.translator::getDefaultLocale()) - MichalSpacekCz\DateTime\DateTimeParser @@ -94,7 +95,7 @@ services: talkVideoThumbnails: MichalSpacekCz\Media\VideoThumbnails(mediaResources: @talkMediaResources) interviewVideoThumbnails: MichalSpacekCz\Media\VideoThumbnails(mediaResources: @interviewMediaResources) - MichalSpacekCz\Net\DnsResolver - - MichalSpacekCz\Pulse\Companies(@database.pulse.explorer) + - MichalSpacekCz\Pulse\Companies(@database.pulse.explorer, @typedDatabase.pulse) - MichalSpacekCz\Pulse\Passwords\Algorithms\PasswordHashingAlgorithms(@database.pulse.explorer, @typedDatabase.pulse) - MichalSpacekCz\Pulse\Passwords\Disclosures\PasswordHashingDisclosures(@database.pulse.explorer, @typedDatabase.pulse) - MichalSpacekCz\Pulse\Passwords\Passwords(@database.pulse.explorer, @typedDatabase.pulse) @@ -102,7 +103,7 @@ services: - MichalSpacekCz\Pulse\Passwords\Rating - MichalSpacekCz\Pulse\Passwords\Storage\StorageAlgorithmAttributesFactory - MichalSpacekCz\Pulse\Passwords\Storage\StorageRegistryFactory - - MichalSpacekCz\Pulse\Sites(@database.pulse.explorer) + - MichalSpacekCz\Pulse\Sites(@database.pulse.explorer, @typedDatabase.pulse) - MichalSpacekCz\Tags\Tags - MichalSpacekCz\Talks\Slides\TalkSlides - MichalSpacekCz\Talks\TalkFactory(videoFactory: @talkVideoFactory) @@ -157,7 +158,7 @@ services: - MichalSpacekCz\Training\Venues\TrainingVenues - MichalSpacekCz\Twitter\TwitterCards - MichalSpacekCz\UpcKeys\Technicolor(@database.upcKeys.explorer, apiUrl: %awsLambda.upcKeys.url%, apiKey: %awsLambda.upcKeys.apiKey%) - - MichalSpacekCz\UpcKeys\Ubee(@database.upcKeys.explorer) + - MichalSpacekCz\UpcKeys\Ubee(@typedDatabase.upcKeys) - MichalSpacekCz\UpcKeys\UpcKeys(routers: [@MichalSpacekCz\UpcKeys\Technicolor, @MichalSpacekCz\UpcKeys\Ubee]) - MichalSpacekCz\User\Manager(passwordEncryption: @passwordEncryption, permanentLoginInterval: %permanentLogin.interval%) - MichalSpacekCz\Utils\Strings diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 4bf2518b1..41bdf04a0 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -485,13 +485,6 @@ - - - key]]> - serial]]> - type]]> - - mac]]> diff --git a/app/src/Articles/Articles.php b/app/src/Articles/Articles.php index 00732a628..8194354ce 100644 --- a/app/src/Articles/Articles.php +++ b/app/src/Articles/Articles.php @@ -103,7 +103,7 @@ public function getAll(?int $limit = null): array ORDER BY published DESC LIMIT ?'; - $articles = $this->database->fetchAll($query, new DateTime(), $this->translator->getDefaultLocale(), $limit ?? PHP_INT_MAX); + $articles = $this->typedDatabase->fetchAll($query, new DateTime(), $this->translator->getDefaultLocale(), $limit ?? PHP_INT_MAX); return $this->enrichArticles(array_values($articles)); } @@ -152,7 +152,7 @@ public function getAllByTags(array $tags, ?int $limit = null): array ORDER BY bp.published DESC LIMIT ?'; - $articles = $this->database->fetchAll($query, $this->tags->serialize($tags), new DateTime(), $this->translator->getDefaultLocale(), $limit ?? PHP_INT_MAX); + $articles = $this->typedDatabase->fetchAll($query, $this->tags->serialize($tags), new DateTime(), $this->translator->getDefaultLocale(), $limit ?? PHP_INT_MAX); return $this->enrichArticles(array_values($articles)); } @@ -178,7 +178,7 @@ public function getAllTags(): array ORDER BY bp.published DESC'; $result = []; - $rows = $this->database->fetchAll($query, new DateTime(), $this->translator->getDefaultLocale()); + $rows = $this->typedDatabase->fetchAll($query, new DateTime(), $this->translator->getDefaultLocale()); foreach ($rows as $row) { $tags = $this->tags->unserialize($row->tags); $slugTags = $this->tags->unserialize($row->slugTags); diff --git a/app/src/Articles/Blog/BlogPostEdits.php b/app/src/Articles/Blog/BlogPostEdits.php index c7e9d0c2e..747b12d5e 100644 --- a/app/src/Articles/Blog/BlogPostEdits.php +++ b/app/src/Articles/Blog/BlogPostEdits.php @@ -4,16 +4,16 @@ namespace MichalSpacekCz\Articles\Blog; use MichalSpacekCz\Articles\ArticleEdit; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\DateTimeZoneFactory; use MichalSpacekCz\DateTime\Exceptions\InvalidTimezoneException; use MichalSpacekCz\Formatter\TexyFormatter; -use Nette\Database\Explorer; readonly class BlogPostEdits { public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, private DateTimeZoneFactory $dateTimeZoneFactory, private TexyFormatter $texyFormatter, ) { @@ -34,7 +34,7 @@ public function getEdits(int $postId): array WHERE key_blog_post = ? ORDER BY edited_at DESC'; $edits = []; - foreach ($this->database->fetchAll($sql, $postId) as $row) { + foreach ($this->typedDatabase->fetchAll($sql, $postId) as $row) { $editedAt = $row->editedAt; $editedAt->setTimezone($this->dateTimeZoneFactory->get($row->editedAtTimezone)); $edits[] = new ArticleEdit($editedAt, $this->texyFormatter->format($row->summaryTexy), $row->summaryTexy); diff --git a/app/src/Articles/Blog/BlogPostLocaleUrls.php b/app/src/Articles/Blog/BlogPostLocaleUrls.php index e88d69d9c..c97b97e62 100644 --- a/app/src/Articles/Blog/BlogPostLocaleUrls.php +++ b/app/src/Articles/Blog/BlogPostLocaleUrls.php @@ -3,15 +3,15 @@ namespace MichalSpacekCz\Articles\Blog; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Tags\Tags; -use Nette\Database\Explorer; use Nette\Utils\JsonException; readonly class BlogPostLocaleUrls { public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, private Tags $tags, ) { } @@ -39,7 +39,7 @@ public function get(string $slug): array WHERE bp.key_translation_group = (SELECT key_translation_group FROM blog_posts WHERE slug = ?) OR bp.slug = ? ORDER BY l.id_locale'; - foreach ($this->database->fetchAll($sql, $slug, $slug) as $row) { + foreach ($this->typedDatabase->fetchAll($sql, $slug, $slug) as $row) { $post = new BlogPostLocaleUrl( $row->locale, $row->slug, diff --git a/app/src/Articles/Blog/BlogPosts.php b/app/src/Articles/Blog/BlogPosts.php index ed5520083..f88e9096c 100644 --- a/app/src/Articles/Blog/BlogPosts.php +++ b/app/src/Articles/Blog/BlogPosts.php @@ -7,6 +7,7 @@ use Exception; use MichalSpacekCz\Articles\Blog\Exceptions\BlogPostDoesNotExistException; use MichalSpacekCz\Articles\Blog\Exceptions\BlogPostWithoutIdException; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\Exceptions\InvalidTimezoneException; use MichalSpacekCz\Tags\Tags; use MichalSpacekCz\Utils\Exceptions\JsonItemNotStringException; @@ -24,6 +25,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private BlogPostLoader $loader, private BlogPostFactory $factory, private Cache $exportsCache, @@ -144,7 +146,7 @@ public function getAll(): array ON l.id_locale = bp.key_locale ORDER BY published, slug'; - foreach ($this->database->fetchAll($sql) as $row) { + foreach ($this->typedDatabase->fetchAll($sql) as $row) { $posts[] = $this->factory->createFromDatabaseRow($row); } return $posts; diff --git a/app/src/Database/TypedDatabase.php b/app/src/Database/TypedDatabase.php index 96a9cad83..6a5dcf8e7 100644 --- a/app/src/Database/TypedDatabase.php +++ b/app/src/Database/TypedDatabase.php @@ -6,6 +6,7 @@ use JetBrains\PhpStorm\Language; use MichalSpacekCz\Database\Exceptions\TypedDatabaseTypeException; use Nette\Database\Explorer; +use Nette\Database\Row; use Nette\Utils\DateTime; readonly class TypedDatabase @@ -164,4 +165,23 @@ public function fetchFieldDateTimeNullable(#[Language('SQL')] string $sql, #[Lan return $field; } + + /** + * @param literal-string $sql + * @param array $params + * @return array + */ + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array + { + $rows = $this->database->fetchAll($sql, ...$params); + $result = []; + foreach ($rows as $row) { + if (!$row instanceof Row) { + throw new TypedDatabaseTypeException(Row::class, $row); + } + $result[] = $row; + } + return $result; + } + } diff --git a/app/src/Interviews/Interviews.php b/app/src/Interviews/Interviews.php index 03c967ea0..807a468cc 100644 --- a/app/src/Interviews/Interviews.php +++ b/app/src/Interviews/Interviews.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Interviews; use DateTime; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Interviews\Exceptions\InterviewDoesNotExistException; use MichalSpacekCz\Media\Exceptions\ContentTypeException; @@ -16,6 +17,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private VideoFactory $videoFactory, private TexyFormatter $texyFormatter, ) { @@ -46,7 +48,7 @@ public function getAll(?int $limit = null): array ORDER BY date DESC LIMIT ?'; - $result = $this->database->fetchAll($query, $limit ?? PHP_INT_MAX); + $result = $this->typedDatabase->fetchAll($query, $limit ?? PHP_INT_MAX); $interviews = []; foreach ($result as $row) { $interviews[] = $this->createFromDatabaseRow($row); diff --git a/app/src/Pulse/Companies.php b/app/src/Pulse/Companies.php index 373e21f17..72e0b89ac 100644 --- a/app/src/Pulse/Companies.php +++ b/app/src/Pulse/Companies.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Pulse; use DateTime; +use MichalSpacekCz\Database\TypedDatabase; use Nette\Database\Explorer; readonly class Companies @@ -11,6 +12,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, ) { } @@ -20,7 +22,7 @@ public function __construct( */ public function getAll(): array { - $rows = $this->database->fetchAll( + $rows = $this->typedDatabase->fetchAll( 'SELECT id, name, diff --git a/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php b/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php index 4fb7d9a38..e4e4ae6d7 100644 --- a/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php +++ b/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php @@ -23,7 +23,7 @@ public function __construct( */ public function getAlgorithms(): array { - $rows = $this->database->fetchAll('SELECT id, algo, alias, salted, stretched FROM password_algos ORDER BY algo'); + $rows = $this->typedDatabase->fetchAll('SELECT id, algo, alias, salted, stretched FROM password_algos ORDER BY algo'); $algorithms = []; foreach ($rows as $row) { $algorithms[] = new PasswordHashingAlgorithm($row->id, $row->algo, $row->alias, (bool)$row->salted, (bool)$row->stretched); diff --git a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php index 6d9094863..d1a6e17d2 100644 --- a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php +++ b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php @@ -24,7 +24,7 @@ public function __construct( */ public function getDisclosureTypes(): array { - $rows = $this->database->fetchAll('SELECT id, alias, type FROM password_disclosure_types ORDER BY type'); + $rows = $this->typedDatabase->fetchAll('SELECT id, alias, type FROM password_disclosure_types ORDER BY type'); $types = []; foreach ($rows as $row) { $types[] = new PasswordHashingDisclosureType($row->id, $row->alias, $row->type); diff --git a/app/src/Pulse/Passwords/Passwords.php b/app/src/Pulse/Passwords/Passwords.php index 7c95b30e8..72f9f4a34 100644 --- a/app/src/Pulse/Passwords/Passwords.php +++ b/app/src/Pulse/Passwords/Passwords.php @@ -73,7 +73,7 @@ public function getAllStorages(?string $rating, string $sort, ?string $search): 'ps.from' => false, 'disclosurePublished' => true, ]; - $storages = $this->storageRegistryFactory->get($this->database->fetchAll($query, $orderBy), $sort); + $storages = $this->storageRegistryFactory->get($this->typedDatabase->fetchAll($query, $orderBy), $sort); $searchMatcher = new SearchMatcher($search, $storages); foreach ($storages->getSites() as $site) { if (($rating !== null && $site->getRating()->name !== $rating) || !$searchMatcher->match($site)) { @@ -133,7 +133,7 @@ public function getStoragesBySite(array $sites): StorageRegistry ps.from DESC, pd.published'; - return $this->storageRegistryFactory->get($this->database->fetchAll($query, $sites), $this->sorting->getDefaultSort()); + return $this->storageRegistryFactory->get($this->typedDatabase->fetchAll($query, $sites), $this->sorting->getDefaultSort()); } @@ -186,7 +186,7 @@ public function getStoragesByCompany(array $companies): StorageRegistry ps.from DESC, pd.published'; - return $this->storageRegistryFactory->get($this->database->fetchAll($query, $companies), $this->sorting->getDefaultSort()); + return $this->storageRegistryFactory->get($this->typedDatabase->fetchAll($query, $companies), $this->sorting->getDefaultSort()); } @@ -233,7 +233,7 @@ public function getStoragesByCompanyId(int $companyId): StorageRegistry ps.from DESC, pd.published'; - return $this->storageRegistryFactory->get($this->database->fetchAll($query, $companyId), $this->sorting->getDefaultSort()); + return $this->storageRegistryFactory->get($this->typedDatabase->fetchAll($query, $companyId), $this->sorting->getDefaultSort()); } diff --git a/app/src/Pulse/Sites.php b/app/src/Pulse/Sites.php index a170b91a1..49b3e5973 100644 --- a/app/src/Pulse/Sites.php +++ b/app/src/Pulse/Sites.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Pulse; use DateTime; +use MichalSpacekCz\Database\TypedDatabase; use Nette\Database\Explorer; readonly class Sites @@ -14,6 +15,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, ) { } @@ -23,7 +25,7 @@ public function __construct( */ public function getAll(): array { - $rows = $this->database->fetchAll('SELECT id, url, alias FROM sites ORDER BY alias'); + $rows = $this->typedDatabase->fetchAll('SELECT id, url, alias FROM sites ORDER BY alias'); $sites = []; foreach ($rows as $row) { $sites[] = new Site($row->id, $row->url, $row->alias); diff --git a/app/src/Talks/Slides/TalkSlides.php b/app/src/Talks/Slides/TalkSlides.php index 0ed23da00..01e9d7681 100644 --- a/app/src/Talks/Slides/TalkSlides.php +++ b/app/src/Talks/Slides/TalkSlides.php @@ -75,7 +75,7 @@ public function getSlideNo(int $talkId, ?string $slide): ?int */ public function getSlides(Talk $talk): TalkSlideCollection { - $slides = $this->database->fetchAll( + $slides = $this->typedDatabase->fetchAll( 'SELECT id_slide AS id, alias, @@ -92,7 +92,7 @@ public function getSlides(Talk $talk): TalkSlideCollection $filenames = []; if ($talk->getFilenamesTalkId() !== null) { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT number, filename, diff --git a/app/src/Talks/Talks.php b/app/src/Talks/Talks.php index 993a79c6d..e8c9da006 100644 --- a/app/src/Talks/Talks.php +++ b/app/src/Talks/Talks.php @@ -69,7 +69,7 @@ public function getAll(?int $limit = null): array LIMIT ?'; $talks = []; - foreach ($this->database->fetchAll($query, $limit ?? PHP_INT_MAX) as $row) { + foreach ($this->typedDatabase->fetchAll($query, $limit ?? PHP_INT_MAX) as $row) { $talks[] = $this->talkFactory->createFromDatabaseRow($row); } return $talks; @@ -129,7 +129,7 @@ public function getUpcoming(): array ORDER BY t.date'; $talks = []; - foreach ($this->database->fetchAll($query) as $row) { + foreach ($this->typedDatabase->fetchAll($query) as $row) { $talks[] = $this->talkFactory->createFromDatabaseRow($row); } return $talks; @@ -254,7 +254,7 @@ public function getFavorites(): array ORDER BY date DESC'; $result = []; - foreach ($this->database->fetchAll($query) as $row) { + foreach ($this->typedDatabase->fetchAll($query) as $row) { $result[] = $this->texyFormatter->substitute($row['favorite'], [$row['title'], $row['action']]); } diff --git a/app/src/Tls/Certificates.php b/app/src/Tls/Certificates.php index 64c6e3c32..9e5b79643 100644 --- a/app/src/Tls/Certificates.php +++ b/app/src/Tls/Certificates.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Tls; use DateTimeImmutable; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\DateTimeFormat; use MichalSpacekCz\DateTime\Exceptions\DateTimeException; use MichalSpacekCz\Tls\Exceptions\CertificateException; @@ -22,6 +23,7 @@ */ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private CertificateFactory $certificateFactory, private array $users, private int $hideExpiredAfter, @@ -72,7 +74,7 @@ public function getNewest(): array ) ORDER BY cr.cn, cr.ext'; $certificates = []; - foreach ($this->database->fetchAll($query) as $data) { + foreach ($this->typedDatabase->fetchAll($query) as $data) { $certificate = $this->certificateFactory->fromDatabaseRow($data); if ($certificate->isExpired() && $certificate->getExpiryDays() > $this->hideExpiredAfter) { continue; diff --git a/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php b/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php index 6d28bc0d7..734e7a00e 100644 --- a/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php +++ b/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\DateTimeZoneFactory; use MichalSpacekCz\DateTime\Exceptions\InvalidTimezoneException; use Nette\Database\Explorer; @@ -20,6 +21,7 @@ class TrainingApplicationStatusHistory public function __construct( private readonly Explorer $database, + private readonly TypedDatabase $typedDatabase, private readonly DateTimeZoneFactory $dateTimeZoneFactory, ) { } @@ -32,7 +34,7 @@ public function __construct( public function getStatusHistory(int $applicationId): array { if (!isset($this->statusHistory[$applicationId])) { - $rows = $this->database->fetchAll( + $rows = $this->typedDatabase->fetchAll( 'SELECT h.id_status_log AS id, h.key_status AS statusId, diff --git a/app/src/Training/Applications/TrainingApplications.php b/app/src/Training/Applications/TrainingApplications.php index 9ba34f374..7da4aa270 100644 --- a/app/src/Training/Applications/TrainingApplications.php +++ b/app/src/Training/Applications/TrainingApplications.php @@ -37,7 +37,7 @@ public function __construct( */ public function getByStatus(TrainingApplicationStatus $status): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT a.id_application AS id, a.name, @@ -114,7 +114,7 @@ public function getByStatus(TrainingApplicationStatus $status): array public function getByDate(int $dateId): array { if (!isset($this->byDate[$dateId])) { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT a.id_application AS id, a.name, diff --git a/app/src/Training/Company/CompanyTrainings.php b/app/src/Training/Company/CompanyTrainings.php index 0e27a1a85..9eeaec916 100644 --- a/app/src/Training/Company/CompanyTrainings.php +++ b/app/src/Training/Company/CompanyTrainings.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Training\Company; use Contributte\Translation\Translator; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Training\Dates\UpcomingTrainingDates; use MichalSpacekCz\Training\Exceptions\CompanyTrainingDoesNotExistException; @@ -17,6 +18,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private TexyFormatter $texyFormatter, private UpcomingTrainingDates $upcomingTrainingDates, private Translator $translator, @@ -74,7 +76,7 @@ public function getInfo(string $name): CompanyTraining */ public function getWithoutPublicUpcoming(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT a.action, t.name diff --git a/app/src/Training/Dates/TrainingDateStatuses.php b/app/src/Training/Dates/TrainingDateStatuses.php index bdeeaee92..475854b7d 100644 --- a/app/src/Training/Dates/TrainingDateStatuses.php +++ b/app/src/Training/Dates/TrainingDateStatuses.php @@ -3,14 +3,14 @@ namespace MichalSpacekCz\Training\Dates; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\ShouldNotHappenException; -use Nette\Database\Explorer; readonly class TrainingDateStatuses { public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, ) { } @@ -20,7 +20,7 @@ public function __construct( */ public function getStatuses(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT s.id_status AS id, s.status, diff --git a/app/src/Training/Dates/TrainingDates.php b/app/src/Training/Dates/TrainingDates.php index 6ebbaca53..802c31045 100644 --- a/app/src/Training/Dates/TrainingDates.php +++ b/app/src/Training/Dates/TrainingDates.php @@ -6,6 +6,7 @@ use Contributte\Translation\Translator; use DateTime; use DateTimeImmutable; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\DateTimeFormatter; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; use MichalSpacekCz\Training\Exceptions\TrainingDateDoesNotExistException; @@ -23,6 +24,7 @@ class TrainingDates public function __construct( private readonly Explorer $database, + private readonly TypedDatabase $typedDatabase, private readonly TrainingApplicationStatuses $trainingApplicationStatuses, private readonly DateTimeFormatter $dateTimeFormatter, private readonly Translator $translator, @@ -94,7 +96,7 @@ public function get(int $dateId): TrainingDate */ public function getWithUnpaid(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT d.id_date AS dateId, t.id_training AS trainingId, @@ -252,7 +254,7 @@ public function add( */ public function getAllTrainings(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT d.id_date AS dateId, t.id_training AS trainingId, @@ -310,7 +312,7 @@ public function getAllTrainings(): array */ public function getAllTrainingsInterval(string $from, string $to = ''): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT d.id_date AS dateId, t.id_training AS trainingId, @@ -375,7 +377,7 @@ public function getPastWithPersonalData(): array return $this->pastWithPersonalData; } - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT DISTINCT d.id_date AS dateId, t.id_training AS trainingId, @@ -450,7 +452,7 @@ public function getPastWithPersonalData(): array */ public function getDates(int $trainingId): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT d.id_date AS dateId, t.id_training AS trainingId, diff --git a/app/src/Training/Dates/UpcomingTrainingDates.php b/app/src/Training/Dates/UpcomingTrainingDates.php index e00120ebe..16e009b1c 100644 --- a/app/src/Training/Dates/UpcomingTrainingDates.php +++ b/app/src/Training/Dates/UpcomingTrainingDates.php @@ -4,7 +4,7 @@ namespace MichalSpacekCz\Training\Dates; use Contributte\Translation\Translator; -use Nette\Database\Explorer; +use MichalSpacekCz\Database\TypedDatabase; class UpcomingTrainingDates { @@ -14,7 +14,7 @@ class UpcomingTrainingDates public function __construct( - private readonly Explorer $database, + private readonly TypedDatabase $typedDatabase, private readonly Translator $translator, private readonly TrainingDateFactory $trainingDateFactory, ) { @@ -129,7 +129,7 @@ private function getUpcoming(bool $includeNonPublic, ?int $venueId = null): arra d.start"; $upcoming = []; - foreach ($this->database->fetchAll($query, $includeNonPublic, $includeNonPublic, TrainingDateStatus::Tentative->value, TrainingDateStatus::Confirmed->value, $this->translator->getDefaultLocale()) as $row) { + foreach ($this->typedDatabase->fetchAll($query, $includeNonPublic, $includeNonPublic, TrainingDateStatus::Tentative->value, TrainingDateStatus::Confirmed->value, $this->translator->getDefaultLocale()) as $row) { if ($venueId !== null && $venueId !== $row->venueId) { continue; } diff --git a/app/src/Training/Discontinued/DiscontinuedTrainings.php b/app/src/Training/Discontinued/DiscontinuedTrainings.php index 28504912f..4f74b3451 100644 --- a/app/src/Training/Discontinued/DiscontinuedTrainings.php +++ b/app/src/Training/Discontinued/DiscontinuedTrainings.php @@ -3,16 +3,16 @@ namespace MichalSpacekCz\Training\Discontinued; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\ShouldNotHappenException; use Nette\Bridges\ApplicationLatte\DefaultTemplate; -use Nette\Database\Explorer; use Nette\Http\IResponse; readonly class DiscontinuedTrainings { public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, private IResponse $httpResponse, ) { } @@ -25,7 +25,7 @@ public function __construct( */ public function getAllDiscontinued(): array { - $query = $this->database->fetchAll( + $query = $this->typedDatabase->fetchAll( 'SELECT td.id_trainings_discontinued AS id, td.description, @@ -65,7 +65,7 @@ public function maybeMarkAsDiscontinued(DefaultTemplate $template, ?int $discont return; } - $query = $this->database->fetchAll( + $query = $this->typedDatabase->fetchAll( 'SELECT td.description, t.name AS training, diff --git a/app/src/Training/Files/TrainingFiles.php b/app/src/Training/Files/TrainingFiles.php index 411363eac..8ebbf8faf 100644 --- a/app/src/Training/Files/TrainingFiles.php +++ b/app/src/Training/Files/TrainingFiles.php @@ -17,7 +17,7 @@ public function __construct( private Explorer $database, - private readonly TypedDatabase $typedDatabase, + private TypedDatabase $typedDatabase, private TrainingApplicationStatuses $trainingApplicationStatuses, private TrainingFileFactory $trainingFileFactory, private TrainingFilesStorage $trainingFilesStorage, @@ -27,7 +27,7 @@ public function __construct( public function getFiles(TrainingApplication $application): TrainingFilesCollection { - $rows = $this->database->fetchAll( + $rows = $this->typedDatabase->fetchAll( 'SELECT f.added, f.id_file AS fileId, diff --git a/app/src/Training/Preliminary/PreliminaryTrainings.php b/app/src/Training/Preliminary/PreliminaryTrainings.php index f5e5f63c0..d2ac9b65b 100644 --- a/app/src/Training/Preliminary/PreliminaryTrainings.php +++ b/app/src/Training/Preliminary/PreliminaryTrainings.php @@ -4,12 +4,12 @@ namespace MichalSpacekCz\Training\Preliminary; use Contributte\Translation\Translator; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Training\Applications\TrainingApplication; use MichalSpacekCz\Training\Applications\TrainingApplicationFactory; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; use MichalSpacekCz\Training\Dates\UpcomingTrainingDates; -use Nette\Database\Explorer; use Nette\Database\Row; use ParagonIE\Halite\Alerts\HaliteAlert; use SodiumException; @@ -18,7 +18,7 @@ { public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, private UpcomingTrainingDates $upcomingTrainingDates, private TrainingApplicationFactory $trainingApplicationFactory, private Translator $translator, @@ -34,7 +34,7 @@ public function __construct( public function getPreliminary(): array { $trainings = []; - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT t.id_training AS idTraining, ua.action, @@ -56,7 +56,7 @@ public function getPreliminary(): array $trainings[$this->createFromDatabaseRow($row)->getId()] = $this->createFromDatabaseRow($row); } - $applications = $this->database->fetchAll( + $applications = $this->typedDatabase->fetchAll( 'SELECT a.id_application AS id, a.name, diff --git a/app/src/Training/Reviews/TrainingReviews.php b/app/src/Training/Reviews/TrainingReviews.php index 525a95f41..ced5fd2a6 100644 --- a/app/src/Training/Reviews/TrainingReviews.php +++ b/app/src/Training/Reviews/TrainingReviews.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Training\Reviews; use DateTime; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Training\Exceptions\TrainingReviewNotFoundException; use Nette\Database\Explorer; @@ -14,6 +15,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private TexyFormatter $texyFormatter, ) { } @@ -48,7 +50,7 @@ public function getVisibleReviews(int $id, ?int $limit = null): array LIMIT ?'; $reviews = []; - foreach ($this->database->fetchAll($query, $id, $id, $limit ?? PHP_INT_MAX) as $row) { + foreach ($this->typedDatabase->fetchAll($query, $id, $id, $limit ?? PHP_INT_MAX) as $row) { $reviews[] = $this->createFromDatabaseRow($row); } return $reviews; @@ -83,7 +85,7 @@ public function getAllReviews(int $id): array ORDER BY r.ranking IS NULL, r.ranking, r.added DESC'; $reviews = []; - foreach ($this->database->fetchAll($query, $id, $id) as $row) { + foreach ($this->typedDatabase->fetchAll($query, $id, $id) as $row) { $reviews[] = $this->createFromDatabaseRow($row); } return $reviews; @@ -145,7 +147,7 @@ public function getReviewsByDateId(int $dateId): array r.key_date = ?'; $reviews = []; - foreach ($this->database->fetchAll($query, $dateId) as $row) { + foreach ($this->typedDatabase->fetchAll($query, $dateId) as $row) { $reviews[] = $this->createFromDatabaseRow($row); } return $reviews; diff --git a/app/src/Training/Trainings/Trainings.php b/app/src/Training/Trainings/Trainings.php index 4b2284de8..050691403 100644 --- a/app/src/Training/Trainings/Trainings.php +++ b/app/src/Training/Trainings/Trainings.php @@ -135,7 +135,7 @@ public function getById(int $id): Training */ public function getNames(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT t.id_training AS id, a.action, @@ -181,7 +181,7 @@ public function getNames(): array */ public function getNamesIncludingCustomDiscontinued(): array { - $result = $this->database->fetchAll( + $result = $this->typedDatabase->fetchAll( 'SELECT t.id_training AS id, a.action, diff --git a/app/src/Training/Venues/TrainingVenues.php b/app/src/Training/Venues/TrainingVenues.php index 09995276d..68f9226de 100644 --- a/app/src/Training/Venues/TrainingVenues.php +++ b/app/src/Training/Venues/TrainingVenues.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Training\Venues; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Training\Exceptions\TrainingVenueNotFoundException; use Nette\Database\Explorer; @@ -13,6 +14,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private TexyFormatter $texyFormatter, ) { } @@ -56,7 +58,7 @@ public function get(string $venueName): TrainingVenue */ public function getAll(): array { - $rows = $this->database->fetchAll( + $rows = $this->typedDatabase->fetchAll( 'SELECT v.id_venue AS id, v.name, diff --git a/app/src/Twitter/TwitterCards.php b/app/src/Twitter/TwitterCards.php index 504093c8b..18a3a82ce 100644 --- a/app/src/Twitter/TwitterCards.php +++ b/app/src/Twitter/TwitterCards.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Twitter; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Twitter\Exceptions\TwitterCardNotFoundException; use Nette\Database\Explorer; use Nette\Database\Row; @@ -12,6 +13,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, ) { } @@ -22,7 +24,7 @@ public function __construct( public function getAll(): array { $cards = []; - $rows = $this->database->fetchAll('SELECT id_twitter_card_type AS cardId, card, title FROM twitter_card_types ORDER BY card'); + $rows = $this->typedDatabase->fetchAll('SELECT id_twitter_card_type AS cardId, card, title FROM twitter_card_types ORDER BY card'); foreach ($rows as $row) { $cards[] = $this->createFromDatabaseRow($row); } diff --git a/app/src/UpcKeys/Technicolor.php b/app/src/UpcKeys/Technicolor.php index 21d0a4b51..a5f976bbd 100644 --- a/app/src/UpcKeys/Technicolor.php +++ b/app/src/UpcKeys/Technicolor.php @@ -5,6 +5,7 @@ use Composer\Pcre\Regex; use DateTime; +use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Http\Client\HttpClient; use MichalSpacekCz\Http\Client\HttpClientRequest; use MichalSpacekCz\Http\Exceptions\HttpClientRequestException; @@ -28,6 +29,7 @@ public function __construct( private Explorer $database, + private TypedDatabase $typedDatabase, private HttpClient $httpClient, private string $apiUrl, private string $apiKey, @@ -117,7 +119,7 @@ private function generateKeys(string $ssid): array */ private function fetchKeys(string $ssid): array { - $rows = $this->database->fetchAll( + $rows = $this->typedDatabase->fetchAll( 'SELECT k.serial, k.key, @@ -130,6 +132,9 @@ private function fetchKeys(string $ssid): array ); $result = []; foreach ($rows as $row) { + assert(is_string($row->serial)); + assert(is_string($row->key)); + assert(is_int($row->type)); $result["{$row->type}-{$row->serial}"] = $this->buildKey($row->serial, $row->key, $row->type); } ksort($result); diff --git a/app/src/UpcKeys/Ubee.php b/app/src/UpcKeys/Ubee.php index 2c78daca9..c2313d3f1 100644 --- a/app/src/UpcKeys/Ubee.php +++ b/app/src/UpcKeys/Ubee.php @@ -3,7 +3,7 @@ namespace MichalSpacekCz\UpcKeys; -use Nette\Database\Explorer; +use MichalSpacekCz\Database\TypedDatabase; use Override; readonly class Ubee implements UpcWiFiRouter @@ -14,7 +14,7 @@ public function __construct( - private Explorer $database, + private TypedDatabase $typedDatabase, ) { } @@ -35,10 +35,12 @@ public function getModelWithPrefixes(): array #[Override] public function getKeys(string $ssid): array { - $rows = $this->database->fetchAll('SELECT mac, `key` FROM keys_ubee WHERE ssid = ?', substr($ssid, 3)); + $rows = $this->typedDatabase->fetchAll('SELECT mac, `key` FROM keys_ubee WHERE ssid = ?', substr($ssid, 3)); $result = []; foreach ($rows as $row) { - $result[$row->mac] = $this->buildKey($row->mac, (int)$row->key); + assert(is_int($row->mac)); + assert(is_int($row->key)); + $result[$row->mac] = $this->buildKey($row->mac, $row->key); } ksort($result); return array_values($result); From d179bbd9a0a3d7c0f9148ffb7cd8db4a51b89b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 17 Nov 2024 17:09:24 +0100 Subject: [PATCH 02/24] Add assert(), isset(), is_array(), docblocks etc. to narrow types --- app/src/Application/Cli/CliArgs.php | 2 +- app/src/Articles/Articles.php | 2 ++ app/src/Http/Client/HttpClient.php | 10 ++++++++- app/src/Pulse/Companies.php | 5 +++++ .../Algorithms/PasswordHashingAlgorithms.php | 5 +++++ .../PasswordHashingDisclosures.php | 3 +++ app/src/Pulse/Sites.php | 3 +++ app/src/Talks/Slides/TalkSlides.php | 10 +++++++++ app/src/Talks/Talks.php | 5 ++++- app/src/Tls/OpenSsl.php | 3 ++- .../TrainingApplicationStatusHistory.php | 5 +++++ .../Training/Dates/TrainingDateStatuses.php | 3 +++ .../Discontinued/DiscontinuedTrainings.php | 22 ++++++++++--------- 13 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/src/Application/Cli/CliArgs.php b/app/src/Application/Cli/CliArgs.php index 1246e8bb2..8a27848d5 100644 --- a/app/src/Application/Cli/CliArgs.php +++ b/app/src/Application/Cli/CliArgs.php @@ -7,7 +7,7 @@ { /** - * @param array $args + * @param array $args */ public function __construct( private array $args, diff --git a/app/src/Articles/Articles.php b/app/src/Articles/Articles.php index 8194354ce..88d0a57ae 100644 --- a/app/src/Articles/Articles.php +++ b/app/src/Articles/Articles.php @@ -180,6 +180,8 @@ public function getAllTags(): array $result = []; $rows = $this->typedDatabase->fetchAll($query, new DateTime(), $this->translator->getDefaultLocale()); foreach ($rows as $row) { + assert(is_string($row->tags)); + assert(is_string($row->slugTags)); $tags = $this->tags->unserialize($row->tags); $slugTags = $this->tags->unserialize($row->slugTags); foreach ($slugTags as $key => $slugTag) { diff --git a/app/src/Http/Client/HttpClient.php b/app/src/Http/Client/HttpClient.php index 0e74587bf..90f7d9441 100644 --- a/app/src/Http/Client/HttpClient.php +++ b/app/src/Http/Client/HttpClient.php @@ -5,6 +5,7 @@ use MichalSpacekCz\Http\Exceptions\HttpClientRequestException; use MichalSpacekCz\Http\Exceptions\HttpStreamException; +use OpenSSLCertificate; class HttpClient { @@ -107,7 +108,14 @@ private function request(HttpClientRequest $request, $context): HttpClientRespon if ($result === false) { throw new HttpClientRequestException($request->getUrl()); } - return new HttpClientResponse($request, $result, $options['ssl']['peer_certificate'] ?? null); + if (is_array($options['ssl'])) { + $certificate = isset($options['ssl']['peer_certificate']) && $options['ssl']['peer_certificate'] instanceof OpenSSLCertificate + ? $options['ssl']['peer_certificate'] + : null; + } else { + $certificate = null; + } + return new HttpClientResponse($request, $result, $certificate); } } diff --git a/app/src/Pulse/Companies.php b/app/src/Pulse/Companies.php index 72e0b89ac..75bb81142 100644 --- a/app/src/Pulse/Companies.php +++ b/app/src/Pulse/Companies.php @@ -34,6 +34,11 @@ public function getAll(): array ); $companies = []; foreach ($rows as $row) { + assert(is_int($row->id)); + assert(is_string($row->name)); + assert(is_string($row->tradeName) || $row->tradeName === null); + assert(is_string($row->alias)); + assert(is_string($row->sortName)); $companies[] = new Company($row->id, $row->name, $row->tradeName, $row->alias, $row->sortName); } return $companies; diff --git a/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php b/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php index e4e4ae6d7..f8368eafe 100644 --- a/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php +++ b/app/src/Pulse/Passwords/Algorithms/PasswordHashingAlgorithms.php @@ -26,6 +26,11 @@ public function getAlgorithms(): array $rows = $this->typedDatabase->fetchAll('SELECT id, algo, alias, salted, stretched FROM password_algos ORDER BY algo'); $algorithms = []; foreach ($rows as $row) { + assert(is_int($row->id)); + assert(is_string($row->algo)); + assert(is_string($row->alias)); + assert(is_int($row->salted)); + assert(is_int($row->stretched)); $algorithms[] = new PasswordHashingAlgorithm($row->id, $row->algo, $row->alias, (bool)$row->salted, (bool)$row->stretched); } return $algorithms; diff --git a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php index d1a6e17d2..57c8ee37a 100644 --- a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php +++ b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php @@ -27,6 +27,9 @@ public function getDisclosureTypes(): array $rows = $this->typedDatabase->fetchAll('SELECT id, alias, type FROM password_disclosure_types ORDER BY type'); $types = []; foreach ($rows as $row) { + assert(is_int($row->id)); + assert(is_string($row->alias)); + assert(is_string($row->type)); $types[] = new PasswordHashingDisclosureType($row->id, $row->alias, $row->type); } return $types; diff --git a/app/src/Pulse/Sites.php b/app/src/Pulse/Sites.php index 49b3e5973..5a70413c0 100644 --- a/app/src/Pulse/Sites.php +++ b/app/src/Pulse/Sites.php @@ -28,6 +28,9 @@ public function getAll(): array $rows = $this->typedDatabase->fetchAll('SELECT id, url, alias FROM sites ORDER BY alias'); $sites = []; foreach ($rows as $row) { + assert(is_int($row->id)); + assert(is_string($row->url)); + assert(is_string($row->alias)); $sites[] = new Site($row->id, $row->url, $row->alias); } return $sites; diff --git a/app/src/Talks/Slides/TalkSlides.php b/app/src/Talks/Slides/TalkSlides.php index 01e9d7681..7dda00862 100644 --- a/app/src/Talks/Slides/TalkSlides.php +++ b/app/src/Talks/Slides/TalkSlides.php @@ -102,12 +102,22 @@ public function getSlides(Talk $talk): TalkSlideCollection $talk->getFilenamesTalkId(), ); foreach ($result as $row) { + assert(is_int($row->number)); + assert(is_string($row->filename)); + assert(is_string($row->filenameAlternative)); $filenames[$row->number] = [$row->filename, $row->filenameAlternative]; } } $result = new TalkSlideCollection($talk->getId()); foreach ($slides as $row) { + assert(is_int($row->id)); + assert(is_string($row->alias)); + assert(is_int($row->number)); + assert(is_string($row->filename)); + assert(is_string($row->filenameAlternative)); + assert(is_string($row->title)); + assert(is_string($row->speakerNotesTexy)); if (isset($filenames[$row->number])) { $filename = $filenames[$row->number][0]; $filenameAlternative = $filenames[$row->number][1]; diff --git a/app/src/Talks/Talks.php b/app/src/Talks/Talks.php index e8c9da006..392d2b4e5 100644 --- a/app/src/Talks/Talks.php +++ b/app/src/Talks/Talks.php @@ -255,7 +255,10 @@ public function getFavorites(): array $result = []; foreach ($this->typedDatabase->fetchAll($query) as $row) { - $result[] = $this->texyFormatter->substitute($row['favorite'], [$row['title'], $row['action']]); + assert(is_string($row->action)); + assert(is_string($row->title)); + assert(is_string($row->favorite)); + $result[] = $this->texyFormatter->substitute($row->favorite, [$row->title, $row->action]); } return $result; diff --git a/app/src/Tls/OpenSsl.php b/app/src/Tls/OpenSsl.php index b37a6783b..c9c80c803 100644 --- a/app/src/Tls/OpenSsl.php +++ b/app/src/Tls/OpenSsl.php @@ -22,8 +22,9 @@ public static function x509parse(OpenSSLCertificate|string $certificate): OpenSs throw new OpenSslException(); } if ( - !isset($info['subject']['commonName'], $info['validFrom_time_t'], $info['validTo_time_t'], $info['serialNumberHex']) + !isset($info['subject']) || !is_array($info['subject']) + || !isset($info['subject']['commonName'], $info['validFrom_time_t'], $info['validTo_time_t'], $info['serialNumberHex']) || !is_string($info['subject']['commonName']) || !is_int($info['validFrom_time_t']) || !is_int($info['validTo_time_t']) diff --git a/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php b/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php index 734e7a00e..12482f11c 100644 --- a/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php +++ b/app/src/Training/ApplicationStatuses/TrainingApplicationStatusHistory.php @@ -49,6 +49,11 @@ public function getStatusHistory(int $applicationId): array ); $items = []; foreach ($rows as $row) { + assert(is_int($row->id)); + assert(is_int($row->statusId)); + assert(is_string($row->status)); + assert($row->statusTime instanceof DateTime); + assert(is_string($row->statusTimeTimeZone)); $items[] = new TrainingApplicationStatusHistoryItem( $row->id, $row->statusId, diff --git a/app/src/Training/Dates/TrainingDateStatuses.php b/app/src/Training/Dates/TrainingDateStatuses.php index 475854b7d..9715d292c 100644 --- a/app/src/Training/Dates/TrainingDateStatuses.php +++ b/app/src/Training/Dates/TrainingDateStatuses.php @@ -31,6 +31,9 @@ public function getStatuses(): array ); $statuses = []; foreach ($result as $row) { + assert(is_int($row->id)); + assert(is_string($row->status)); + assert(is_string($row->description)); $status = TrainingDateStatus::from($row->status); if ($status->id() !== $row->id || $status->description() !== $row->description) { throw new ShouldNotHappenException("Training data status enum doesn't match database values for status '{$status->value}'"); diff --git a/app/src/Training/Discontinued/DiscontinuedTrainings.php b/app/src/Training/Discontinued/DiscontinuedTrainings.php index 4f74b3451..6b4eba54f 100644 --- a/app/src/Training/Discontinued/DiscontinuedTrainings.php +++ b/app/src/Training/Discontinued/DiscontinuedTrainings.php @@ -4,7 +4,6 @@ namespace MichalSpacekCz\Training\Discontinued; use MichalSpacekCz\Database\TypedDatabase; -use MichalSpacekCz\ShouldNotHappenException; use Nette\Bridges\ApplicationLatte\DefaultTemplate; use Nette\Http\IResponse; @@ -39,16 +38,16 @@ public function getAllDiscontinued(): array ); $trainings = []; foreach ($query as $row) { - $id = $row->id; - if (!is_int($id)) { - throw new ShouldNotHappenException(sprintf("Discontinued training id is a %s not an integer", get_debug_type($id))); + assert(is_int($row->id)); + assert(is_string($row->description)); + assert(is_string($row->href)); + assert(is_string($row->training)); + $trainings[$row->id]['description'] = $row->description; + $trainings[$row->id]['href'] = $row->href; + if (!isset($trainings[$row->id]['trainings'])) { + $trainings[$row->id]['trainings'] = []; } - $trainings[$id]['description'] = (string)$row->description; - $trainings[$id]['href'] = (string)$row->href; - if (!isset($trainings[$id]['trainings'])) { - $trainings[$id]['trainings'] = []; - } - $trainings[$id]['trainings'][] = (string)$row->training; + $trainings[$row->id]['trainings'][] = $row->training; } $result = []; foreach ($trainings as $training) { @@ -80,9 +79,12 @@ public function maybeMarkAsDiscontinued(DefaultTemplate $template, ?int $discont ); $trainings = []; foreach ($query as $row) { + assert(is_string($row->training)); $trainings[] = $row->training; } if (isset($row)) { + assert(is_string($row->description)); + assert(is_string($row->href)); $template->discontinued = [new DiscontinuedTraining($row->description, $trainings, $row->href)]; $this->httpResponse->setCode(IResponse::S410_Gone); return; From 22332481eafec24bf687b7500c26b3196b088eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 18 Nov 2024 05:03:24 +0100 Subject: [PATCH 03/24] Use assert() instead of ShouldNotHappenException in tests --- app/tests/Application/BootstrapTest.phpt | 5 +---- .../Form/SignInHoneypotFormFactoryTest.phpt | 12 ++++++------ app/tests/Http/HttpClientTest.phpt | 1 + .../TrainingApplicationFormSuccessTest.phpt | 16 ++++------------ .../Discontinued/DiscontinuedTrainingsTest.phpt | 2 +- app/tests/User/ManagerTest.phpt | 5 +---- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/app/tests/Application/BootstrapTest.phpt b/app/tests/Application/BootstrapTest.phpt index 9788358d8..030b229a0 100644 --- a/app/tests/Application/BootstrapTest.phpt +++ b/app/tests/Application/BootstrapTest.phpt @@ -4,7 +4,6 @@ declare(strict_types = 1); namespace MichalSpacekCz\Application; -use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\TestCaseRunner; use Nette\DI\Container; use Tester\Assert; @@ -25,9 +24,7 @@ class BootstrapTest extends TestCase public function __construct() { $logDirectory = Debugger::$logDirectory; - if ($logDirectory === null) { - throw new ShouldNotHappenException('Call Nette\Bootstrap\Configurator::enableTracy() first, possibly in MichalSpacekCz\Application\Bootstrap::createConfigurator()'); - } + assert(is_string($logDirectory), 'Call Nette\Bootstrap\Configurator::enableTracy() first, possibly in MichalSpacekCz\Application\Bootstrap::createConfigurator()'); $this->exceptionLog = $logDirectory . '/' . ILogger::EXCEPTION . '.log'; if (file_exists($this->exceptionLog)) { $this->tempLog = $this->exceptionLog . '.' . uniqid(more_entropy: true); diff --git a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt index 5ce7e354e..8cfa46af9 100644 --- a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt +++ b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt @@ -9,6 +9,7 @@ use MichalSpacekCz\Test\TestCaseRunner; use Nette\Forms\Controls\TextInput; use Nette\Utils\Arrays; use Override; +use Stringable; use Tester\Assert; use Tester\TestCase; @@ -59,18 +60,17 @@ class SignInHoneypotFormFactoryTest extends TestCase $this->setValue('username', $username); $this->setValue('password', $password); Arrays::invoke($this->form->onSuccess, $this->form); - Assert::same($error, (string)$this->form->getErrors()[0]); + $formError = $this->form->getErrors()[0]; + assert(is_string($formError) || $formError instanceof Stringable); + Assert::same($error, (string)$formError); } private function setValue(string $component, string $value): void { $field = $this->form->getComponent($component); - if (!$field instanceof TextInput) { - Assert::fail('Field is of a wrong type ' . $field::class); - } else { - $field->setDefaultValue($value); - } + assert($field instanceof TextInput); + $field->setDefaultValue($value); } } diff --git a/app/tests/Http/HttpClientTest.phpt b/app/tests/Http/HttpClientTest.phpt index d3187940f..6d879014a 100644 --- a/app/tests/Http/HttpClientTest.phpt +++ b/app/tests/Http/HttpClientTest.phpt @@ -71,6 +71,7 @@ class HttpClientTest extends TestCase ], ]; Assert::same($expected, $params['options']); + assert(is_callable($params['notification'])); Assert::noError(function () use ($params): void { call_user_func($params['notification'], 303, STREAM_NOTIFY_SEVERITY_INFO, 'ok', 808); }); diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt index e9fbe3183..3a368a403 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt @@ -8,7 +8,6 @@ namespace MichalSpacekCz\Training\ApplicationForm; use DateTime; use MichalSpacekCz\Form\Controls\TrainingControlsFactory; use MichalSpacekCz\Form\UiForm; -use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\Http\NullSession; use MichalSpacekCz\Test\NullMailer; @@ -68,9 +67,7 @@ class TrainingApplicationFormSuccessTest extends TestCase NullSession $session, ) { $presenter = $presenterFactory->createPresenter('Www:Homepage'); // Has to be a real presenter that extends Ui\Presenter - if (!$presenter instanceof Presenter) { - throw new ShouldNotHappenException(); - } + assert($presenter instanceof Presenter); PrivateProperty::setValue($application, 'presenter', $presenter); $this->form = new UiForm($presenter, 'form'); $trainingControlsFactory->addAttendee($this->form); @@ -91,9 +88,7 @@ class TrainingApplicationFormSuccessTest extends TestCase ]); $this->sessionSection = $session->getSection('section', TrainingApplicationSessionSection::class); $parentClass = (new ReflectionClass($this->sessionSection))->getParentClass(); - if (!$parentClass) { - throw new ShouldNotHappenException(sprintf('Parent class of %s should exist', $this->sessionSection::class)); - } + assert($parentClass instanceof ReflectionClass); $this->sessionSectionParentGet = $parentClass->getMethod('get'); $this->sessionSectionParentSet = $parentClass->getMethod('set'); } @@ -275,11 +270,8 @@ class TrainingApplicationFormSuccessTest extends TestCase private function sessionSectionGet(string $name): string|int|array { $result = $this->sessionSectionParentGet->invoke($this->sessionSection, $name); - if (!is_string($result) && !is_int($result) && !is_array($result)) { - throw new ShouldNotHappenException(sprintf('Session data type is %s, but should be string|int|array', get_debug_type($result))); - } else { - return $result; - } + assert(is_string($result) || is_int($result) || is_array($result)); + return $result; } } diff --git a/app/tests/Training/Discontinued/DiscontinuedTrainingsTest.phpt b/app/tests/Training/Discontinued/DiscontinuedTrainingsTest.phpt index 749f78d48..5430a784c 100644 --- a/app/tests/Training/Discontinued/DiscontinuedTrainingsTest.phpt +++ b/app/tests/Training/Discontinued/DiscontinuedTrainingsTest.phpt @@ -95,7 +95,7 @@ class DiscontinuedTrainingsTest extends TestCase ], ]); $this->discontinuedTrainings->maybeMarkAsDiscontinued($template, 302); - Assert::type(DiscontinuedTraining::class, $template->discontinued[0]); + assert(is_array($template->discontinued) && $template->discontinued[0] instanceof DiscontinuedTraining); Assert::same('foo', $template->discontinued[0]->getDescription()); Assert::same(['intro', 'classes'], $template->discontinued[0]->getTrainings()); Assert::same('https://foo.example', $template->discontinued[0]->getNewHref()); diff --git a/app/tests/User/ManagerTest.phpt b/app/tests/User/ManagerTest.phpt index 67e4197f0..80cc87825 100644 --- a/app/tests/User/ManagerTest.phpt +++ b/app/tests/User/ManagerTest.phpt @@ -7,7 +7,6 @@ declare(strict_types = 1); namespace MichalSpacekCz\User; use MichalSpacekCz\Http\Cookies\CookieName; -use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\Http\Request; use MichalSpacekCz\Test\PrivateProperty; @@ -45,9 +44,7 @@ class ManagerTest extends TestCase Container $container, ) { $service = $container->getService('passwordEncryption'); - if (!$service instanceof SymmetricKeyEncryption) { - throw new ShouldNotHappenException(sprintf('passwordEncryption should be a %s instance, but it is a %s', SymmetricKeyEncryption::class, $service::class)); - } + assert($service instanceof SymmetricKeyEncryption); $this->passwordEncryption = $service; } From fb569d74497fab1d65f19ecb67e9ba3a910fbe53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 18 Nov 2024 06:32:25 +0100 Subject: [PATCH 04/24] Use Composer\Pcre\Preg::split() instead of Nette\Utils\Strings::split() for better type support --- app/disallowed-calls.neon | 1 + app/phpstan-vendor.neon | 1 + app/src/Formatter/TexyPhraseHandler.php | 4 ++-- app/src/Tags/Tags.php | 3 ++- .../Test/Application/LocaleLinkGeneratorMock.php | 13 +++++++++++++ app/tests/Formatter/TexyPhraseHandlerTest.phpt | 6 ++++-- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/disallowed-calls.neon b/app/disallowed-calls.neon index 0d38fa247..56407d53c 100644 --- a/app/disallowed-calls.neon +++ b/app/disallowed-calls.neon @@ -29,6 +29,7 @@ parameters: - 'Nette\Utils\Strings::match()' - 'Nette\Utils\Strings::matchAll()' - 'Nette\Utils\Strings::replace()' + - 'Nette\Utils\Strings::split()' message: 'use the Preg or Regex class from composer/pcre for better static analysis' disallowedMethodCalls: - diff --git a/app/phpstan-vendor.neon b/app/phpstan-vendor.neon index 5a6fc7bc6..b91c92947 100644 --- a/app/phpstan-vendor.neon +++ b/app/phpstan-vendor.neon @@ -218,6 +218,7 @@ parameters: - 'Nette\Utils\Strings::match()' - 'Nette\Utils\Strings::matchAll()' - 'Nette\Utils\Strings::replace()' + - 'Nette\Utils\Strings::split()' allowIn: - vendor/contributte/*.php - vendor/nette/*.php diff --git a/app/src/Formatter/TexyPhraseHandler.php b/app/src/Formatter/TexyPhraseHandler.php index 365b0b794..4eecb0748 100644 --- a/app/src/Formatter/TexyPhraseHandler.php +++ b/app/src/Formatter/TexyPhraseHandler.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Formatter; +use Composer\Pcre\Preg; use Composer\Pcre\Regex; use Contributte\Translation\Translator; use MichalSpacekCz\Application\Locale\LocaleLinkGenerator; @@ -16,7 +17,6 @@ use Nette\Application\UI\Presenter; use Nette\Utils\Arrays; use Nette\Utils\Html; -use Nette\Utils\Strings; use Texy\HandlerInvocation; use Texy\HtmlElement; use Texy\Link; @@ -109,7 +109,7 @@ public function solve(HandlerInvocation $invocation, string $phrase, string $con private function getLink(string $url, string $locale): string { - $args = Strings::split($url, '/[\s,]+/'); + $args = Preg::split('/[\s,]+/', $url); $action = array_shift($args); if (Arrays::contains([self::TRAINING_ACTION, self::COMPANY_TRAINING_ACTION], $action)) { $args = [$this->trainingLocales->getLocaleActions($args[0])[$locale]]; diff --git a/app/src/Tags/Tags.php b/app/src/Tags/Tags.php index fc5264624..7ff116829 100644 --- a/app/src/Tags/Tags.php +++ b/app/src/Tags/Tags.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Tags; +use Composer\Pcre\Preg; use MichalSpacekCz\Utils\Exceptions\JsonItemNotStringException; use MichalSpacekCz\Utils\Exceptions\JsonItemsNotArrayException; use MichalSpacekCz\Utils\JsonUtils; @@ -27,7 +28,7 @@ public function __construct( */ public function toArray(string $tags): array { - $values = Strings::split($tags, '/\s*,\s*/'); + $values = Preg::split('/\s*,\s*/', $tags); return array_values(array_filter($values)); } diff --git a/app/src/Test/Application/LocaleLinkGeneratorMock.php b/app/src/Test/Application/LocaleLinkGeneratorMock.php index c493da4fc..ccc08f25a 100644 --- a/app/src/Test/Application/LocaleLinkGeneratorMock.php +++ b/app/src/Test/Application/LocaleLinkGeneratorMock.php @@ -12,6 +12,9 @@ class LocaleLinkGeneratorMock extends LocaleLinkGenerator /** @var array */ private array $allLinks = []; + /** @var array> */ + private array $allLinksParams = []; + /** @noinspection PhpMissingParentConstructorInspection Intentionally */ public function __construct() @@ -52,7 +55,17 @@ public function setAllLinks(array $allLinks): void #[Override] public function allLinks(string $destination, array $params = []): array { + $this->allLinksParams = $params; return $this->allLinks; } + + /** + * @return array> + */ + public function getAllLinksParams(): array + { + return $this->allLinksParams; + } + } diff --git a/app/tests/Formatter/TexyPhraseHandlerTest.phpt b/app/tests/Formatter/TexyPhraseHandlerTest.phpt index 49d861add..d84b3bd04 100644 --- a/app/tests/Formatter/TexyPhraseHandlerTest.phpt +++ b/app/tests/Formatter/TexyPhraseHandlerTest.phpt @@ -66,8 +66,10 @@ class TexyPhraseHandlerTest extends TestCase $this->defaultLocale => $defaultLocaleUrl, self::EN_LOCALE => $enLocaleUrl, ]); - $this->assertUrl('title', $defaultLocaleUrl, '"title":[link:Module:Presenter:action params]'); - $this->assertUrl('title', $enLocaleUrl, '"title":[link-' . self::EN_LOCALE . ':Module:Presenter:action params]'); + $this->assertUrl('title', $defaultLocaleUrl, '"title":[link:Module:Presenter:action params,foo]'); + Assert::same(['params', 'foo'], $this->localeLinkGenerator->getAllLinksParams()[$this->defaultLocale]); + $this->assertUrl('title', $enLocaleUrl, '"title":[link-' . self::EN_LOCALE . ':Module:Presenter:action params , bar]'); + Assert::same(['params', 'bar'], $this->localeLinkGenerator->getAllLinksParams()[self::EN_LOCALE]); } From 3cd3d36d349ea3b5133d22eb81492dadcc35087b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 19 Nov 2024 06:48:04 +0100 Subject: [PATCH 05/24] Presenter action params can also be a list --- app/psalm-baseline.xml | 2 -- app/src/Application/Locale/LocaleLinkGenerator.php | 12 ++++++------ app/src/Formatter/TexyPhraseHandler.php | 2 +- app/src/Test/Application/LocaleLinkGeneratorMock.php | 4 ++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 41bdf04a0..a92affeed 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -290,8 +290,6 @@ - - diff --git a/app/src/Application/Locale/LocaleLinkGenerator.php b/app/src/Application/Locale/LocaleLinkGenerator.php index 438432540..78afbc686 100644 --- a/app/src/Application/Locale/LocaleLinkGenerator.php +++ b/app/src/Application/Locale/LocaleLinkGenerator.php @@ -40,7 +40,7 @@ public function __construct( * Generates localized URLs. * * @param string $destination destination in format "[[[module:]presenter:]action] [#fragment]" - * @param array> $params of locale => [name => value] + * @param array|array> $params of locale => [position|name => value] * @return array of locale => URL * @throws InvalidLinkException */ @@ -82,8 +82,8 @@ public function defaultParams(array $params): array /** * Set default params. * - * @param array> $params - * @param array $defaultParams + * @param array|array> $params + * @param list|array $defaultParams */ public function setDefaultParams(array &$params, array $defaultParams): void { @@ -95,7 +95,7 @@ public function setDefaultParams(array &$params, array $defaultParams): void * Generates all URLs, including a link to the current language version. * * @param string $destination destination in format "[[[module:]presenter:]action] [#fragment]" - * @param array> $params of locale => [name => value] + * @param array|array> $params of locale => [position|name => value] * @return array of locale => URL */ public function allLinks(string $destination, array $params = []): array @@ -114,9 +114,9 @@ public function allLinks(string $destination, array $params = []): array /** - * @param array> $params + * @param array|array> $params * @param string $locale - * @return array + * @return list|array */ private function getParams(array $params, string $locale): array { diff --git a/app/src/Formatter/TexyPhraseHandler.php b/app/src/Formatter/TexyPhraseHandler.php index 4eecb0748..955a1e0a5 100644 --- a/app/src/Formatter/TexyPhraseHandler.php +++ b/app/src/Formatter/TexyPhraseHandler.php @@ -135,7 +135,7 @@ private function getBlogLink(string $url, string $locale): string /** - * @param non-empty-array> $params + * @param non-empty-array|array> $params */ private function getLinkWithParams(string $destination, array $params, string $locale): string { diff --git a/app/src/Test/Application/LocaleLinkGeneratorMock.php b/app/src/Test/Application/LocaleLinkGeneratorMock.php index ccc08f25a..521c16a51 100644 --- a/app/src/Test/Application/LocaleLinkGeneratorMock.php +++ b/app/src/Test/Application/LocaleLinkGeneratorMock.php @@ -12,7 +12,7 @@ class LocaleLinkGeneratorMock extends LocaleLinkGenerator /** @var array */ private array $allLinks = []; - /** @var array> */ + /** @var array|array> */ private array $allLinksParams = []; @@ -61,7 +61,7 @@ public function allLinks(string $destination, array $params = []): array /** - * @return array> + * @return array|array> */ public function getAllLinksParams(): array { From e29721210a57ac81ad4329559bfd9ae5b5e0f2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 19 Nov 2024 06:36:13 +0100 Subject: [PATCH 06/24] Ensure presenter action parameters are strings --- app/config/services.neon | 1 + app/src/Application/AppRequest.php | 21 +++++++++ app/src/Application/ComponentParameters.php | 30 ++++++++++++ .../ParameterNotStringException.php | 16 +++++++ app/src/Www/Presenters/BasePresenter.php | 20 +++++++- .../Presenters/CompanyTrainingsPresenter.php | 6 ++- app/src/Www/Presenters/ErrorPresenter.php | 6 ++- app/src/Www/Presenters/TrainingsPresenter.php | 6 ++- app/tests/Application/AppRequestTest.phpt | 21 +++++++++ .../Application/ComponentParametersTest.phpt | 46 +++++++++++++++++++ 10 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 app/src/Application/ComponentParameters.php create mode 100644 app/src/Application/Exceptions/ParameterNotStringException.php create mode 100644 app/tests/Application/ComponentParametersTest.phpt diff --git a/app/config/services.neon b/app/config/services.neon index 9cdceeff3..97d5ba442 100644 --- a/app/config/services.neon +++ b/app/config/services.neon @@ -3,6 +3,7 @@ services: cliArgs: type: MichalSpacekCz\Application\Cli\CliArgs imported: true + - MichalSpacekCz\Application\ComponentParameters - MichalSpacekCz\Application\Error - MichalSpacekCz\Application\LinkGenerator localeLinkGenerator: MichalSpacekCz\Application\Locale\LocaleLinkGenerator(languages: %locales.languages%) diff --git a/app/src/Application/AppRequest.php b/app/src/Application/AppRequest.php index f4dc82fae..a588f85fc 100644 --- a/app/src/Application/AppRequest.php +++ b/app/src/Application/AppRequest.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Application; use MichalSpacekCz\Application\Exceptions\NoOriginalRequestException; +use MichalSpacekCz\Application\Exceptions\ParameterNotStringException; use MichalSpacekCz\ShouldNotHappenException; use Nette\Application\Request; use Throwable; @@ -27,6 +28,26 @@ public function getOriginalRequest(?Request $request): Request } + /** + * @return array + * @throws NoOriginalRequestException + * @throws ParameterNotStringException + */ + public function getOriginalRequestStringParameters(?Request $request): array + { + $params = []; + foreach ($this->getOriginalRequest($request)->getParameters() as $name => $value) { + $name = (string)$name; + if ($value === null || is_string($value)) { + $params[$name] = $value; + } else { + throw new ParameterNotStringException($name, get_debug_type($value)); + } + } + return $params; + } + + public function getException(Request $request): Throwable { $e = $request->getParameter('exception'); diff --git a/app/src/Application/ComponentParameters.php b/app/src/Application/ComponentParameters.php new file mode 100644 index 000000000..bbb14c96b --- /dev/null +++ b/app/src/Application/ComponentParameters.php @@ -0,0 +1,30 @@ + + * @throws ParameterNotStringException + */ + public function getStringParameters(Component $component): array + { + $params = []; + foreach ($component->getParameters() as $name => $value) { + $name = (string)$name; + if ($value === null || is_string($value)) { + $params[$name] = $value; + } else { + throw new ParameterNotStringException($name, get_debug_type($value)); + } + } + return $params; + } + +} diff --git a/app/src/Application/Exceptions/ParameterNotStringException.php b/app/src/Application/Exceptions/ParameterNotStringException.php new file mode 100644 index 000000000..6063d7f25 --- /dev/null +++ b/app/src/Application/Exceptions/ParameterNotStringException.php @@ -0,0 +1,16 @@ +componentParameters = $componentParameters; + } + + #[Override] protected function startup(): void { @@ -91,6 +104,9 @@ protected function startup(): void } + /** + * @throws ParameterNotStringException + */ #[Override] public function beforeRender(): void { @@ -110,6 +126,7 @@ protected function getLocaleLinksGeneratorDestination(): string /** * @return array> + * @throws ParameterNotStringException */ protected function getLocaleLinksGeneratorParams(): array { @@ -141,10 +158,11 @@ protected function getLocaleLinkAction(): string * Default parameters for locale links. * * @return array> + * @throws ParameterNotStringException */ protected function getLocaleLinkParams(): array { - return $this->localeLinkGenerator->defaultParams($this->getParameters()); + return $this->localeLinkGenerator->defaultParams($this->componentParameters->getStringParameters($this)); } diff --git a/app/src/Www/Presenters/CompanyTrainingsPresenter.php b/app/src/Www/Presenters/CompanyTrainingsPresenter.php index b97b7fa7b..91e78f002 100644 --- a/app/src/Www/Presenters/CompanyTrainingsPresenter.php +++ b/app/src/Www/Presenters/CompanyTrainingsPresenter.php @@ -4,6 +4,8 @@ namespace MichalSpacekCz\Www\Presenters; use Contributte\Translation\Translator; +use MichalSpacekCz\Application\ComponentParameters; +use MichalSpacekCz\Application\Exceptions\ParameterNotStringException; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Training\Company\CompanyTrainings; use MichalSpacekCz\Training\Discontinued\DiscontinuedTrainings; @@ -30,6 +32,7 @@ public function __construct( private readonly TrainingReviews $trainingReviews, private readonly Prices $prices, private readonly Translator $translator, + private readonly ComponentParameters $componentParameters, ) { parent::__construct(); } @@ -70,11 +73,12 @@ public function actionTraining(string $name): void * Translated locale parameters for trainings. * * @return array> + * @throws ParameterNotStringException */ #[Override] protected function getLocaleLinkParams(): array { - return $this->trainingLocales->getLocaleLinkParams($this->trainingAction, $this->getParameters()); + return $this->trainingLocales->getLocaleLinkParams($this->trainingAction, $this->componentParameters->getStringParameters($this)); } } diff --git a/app/src/Www/Presenters/ErrorPresenter.php b/app/src/Www/Presenters/ErrorPresenter.php index b94048c6f..e60930411 100644 --- a/app/src/Www/Presenters/ErrorPresenter.php +++ b/app/src/Www/Presenters/ErrorPresenter.php @@ -6,6 +6,7 @@ use Contributte\Translation\Translator; use MichalSpacekCz\Application\AppRequest; use MichalSpacekCz\Application\Exceptions\NoOriginalRequestException; +use MichalSpacekCz\Application\Exceptions\ParameterNotStringException; use MichalSpacekCz\Application\Locale\LocaleLink; use MichalSpacekCz\Application\Locale\LocaleLinkGenerator; use MichalSpacekCz\EasterEgg\FourOhFourButFound; @@ -116,12 +117,13 @@ protected function getLocaleLinkAction(): string * * @return array> * @throws NoOriginalRequestException + * @throws ParameterNotStringException */ #[Override] protected function getLocaleLinkParams(): array { - $requestParam = $this->appRequest->getOriginalRequest($this->getRequest()); - return $this->localeLinkGenerator->defaultParams($requestParam->getParameters()); + $params = $this->appRequest->getOriginalRequestStringParameters($this->getRequest()); + return $this->localeLinkGenerator->defaultParams($params); } } diff --git a/app/src/Www/Presenters/TrainingsPresenter.php b/app/src/Www/Presenters/TrainingsPresenter.php index 67f078536..65afb4369 100644 --- a/app/src/Www/Presenters/TrainingsPresenter.php +++ b/app/src/Www/Presenters/TrainingsPresenter.php @@ -4,6 +4,8 @@ namespace MichalSpacekCz\Www\Presenters; use Contributte\Translation\Translator; +use MichalSpacekCz\Application\ComponentParameters; +use MichalSpacekCz\Application\Exceptions\ParameterNotStringException; use MichalSpacekCz\CompanyInfo\CompanyInfo; use MichalSpacekCz\Form\TrainingApplicationFormFactory; use MichalSpacekCz\Form\TrainingApplicationPreliminaryFormFactory; @@ -64,6 +66,7 @@ public function __construct( private readonly Translator $translator, private readonly Session $sessionHandler, private readonly Robots $robots, + private readonly ComponentParameters $componentParameters, ) { parent::__construct(); } @@ -306,11 +309,12 @@ protected function createComponentOtherUpcomingDatesList(): UpcomingTrainingDate * Translated locale parameters for trainings. * * @return array> + * @throws ParameterNotStringException */ #[Override] protected function getLocaleLinkParams(): array { - return $this->trainingLocales->getLocaleLinkParams($this->trainingAction, $this->getParameters()); + return $this->trainingLocales->getLocaleLinkParams($this->trainingAction, $this->componentParameters->getStringParameters($this)); } diff --git a/app/tests/Application/AppRequestTest.phpt b/app/tests/Application/AppRequestTest.phpt index 780dabfa3..c4908f5b6 100644 --- a/app/tests/Application/AppRequestTest.phpt +++ b/app/tests/Application/AppRequestTest.phpt @@ -8,6 +8,7 @@ use DateTime; use Error; use Exception; use MichalSpacekCz\Application\Exceptions\NoOriginalRequestException; +use MichalSpacekCz\Application\Exceptions\ParameterNotStringException; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\TestCaseRunner; use Nette\Application\Request; @@ -63,6 +64,26 @@ class AppRequestTest extends TestCase } + public function testGetOriginalRequestStringParameters(): void + { + $original = new Request('bar', params: ['foo' => 'bar', 1 => 'one']); + $request = new Request('foo'); + $request->setParameters(['request' => $original]); + Assert::same(['foo' => 'bar', '1' => 'one'], $this->appRequest->getOriginalRequestStringParameters($request)); + } + + + public function testGetOriginalRequestStringParametersException(): void + { + $original = new Request('bar', params: ['foo' => 'bar', 'one' => 1]); + $request = new Request('foo'); + $request->setParameters(['request' => $original]); + Assert::exception(function () use ($request): void { + $this->appRequest->getOriginalRequestStringParameters($request); + }, ParameterNotStringException::class, "Component parameter 'one' is not a string but it's a int"); + } + + public function testGetExceptionNoException(): void { Assert::exception(function (): void { diff --git a/app/tests/Application/ComponentParametersTest.phpt b/app/tests/Application/ComponentParametersTest.phpt new file mode 100644 index 000000000..ee9a441b6 --- /dev/null +++ b/app/tests/Application/ComponentParametersTest.phpt @@ -0,0 +1,46 @@ +component = new class extends Component { + }; + } + + + public function testGetStringParameters(): void + { + $this->component->loadState([]); + Assert::same([], $this->componentParameters->getStringParameters($this->component)); + + $this->component->loadState(['foo' => 'bar', 'baz' => 'quux', 1 => 'one']); + Assert::same(['foo' => 'bar', 'baz' => 'quux', '1' => 'one'], $this->componentParameters->getStringParameters($this->component)); + + $this->component->loadState(['foo' => 'bar', 'number' => 1, 'baz' => 'quux']); + Assert::exception(function (): void { + $this->componentParameters->getStringParameters($this->component); + }, ParameterNotStringException::class, "Component parameter 'number' is not a string but it's a int"); + } + +} + +TestCaseRunner::run(ComponentParametersTest::class); From 436ff506f73b0b95e265173dededbe319aa0a390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Wed, 20 Nov 2024 04:25:19 +0100 Subject: [PATCH 07/24] Log only string values in training application logger --- .../ApplicationForm/TrainingApplicationFormDataLogger.php | 2 +- .../ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php b/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php index 67f579a2a..ab8548a54 100644 --- a/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php +++ b/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php @@ -16,7 +16,7 @@ public function log(stdClass $values, string $name, int $dateId, ?TrainingApplic $logSession = $applicationId !== null ? "id => '{$applicationId}', dateId => '{$dateId}'" : null; $logValues = []; foreach ((array)$values as $key => $value) { - $logValues[] = "{$key} => '{$value}'"; + $logValues[] = sprintf('%s => %s', $key, is_string($value) ? "'{$value}'" : get_debug_type($value)); } $message = sprintf( 'Application session data for %s: %s, form values: %s', diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt index 76089d074..0b4b25a66 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt @@ -80,12 +80,13 @@ class TrainingApplicationFormDataLoggerTest extends TestCase $values = new stdClass(); $values->key1 = 'value1'; $values->key2 = 'value2'; + $values->key3 = 1336; $trainingName = 'foo'; $session = $this->getTrainingSessionSection(); $session->setApplicationForTraining($trainingName, $this->getApplication()); $this->formDataLogger->log($values, $trainingName, self::DATE_ID, $session); - $expected = sprintf("Application session data for foo: id => '%s', dateId => '%s', form values: key1 => 'value1', key2 => 'value2'", self::APPLICATION_ID, self::DATE_ID); + $expected = sprintf("Application session data for foo: id => '%s', dateId => '%s', form values: key1 => 'value1', key2 => 'value2', key3 => int", self::APPLICATION_ID, self::DATE_ID); Assert::same([$expected], $this->logger->getLogged()); } From 64a8515d71ac54a6e21424e0983124a22e6f994a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Thu, 21 Nov 2024 03:01:57 +0100 Subject: [PATCH 08/24] Add test for Pulse\Passwords\PasswordsSorting --- .../Pulse/Passwords/PasswordsSortingTest.phpt | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 app/tests/Pulse/Passwords/PasswordsSortingTest.phpt diff --git a/app/tests/Pulse/Passwords/PasswordsSortingTest.phpt b/app/tests/Pulse/Passwords/PasswordsSortingTest.phpt new file mode 100644 index 000000000..17c31f58a --- /dev/null +++ b/app/tests/Pulse/Passwords/PasswordsSortingTest.phpt @@ -0,0 +1,163 @@ +>}> + */ + public function getExpected(): array + { + return [ + 'rating A-F' => [ + 'rating-a-f', + [ + 'Company A' => [10 => 'A'], + 'Company B' => [20 => 'A'], + 'Company C' => [40 => 'A', 30 => 'A'], + ], + ], + 'rating F-A' => [ + 'rating-f-a', + [ + 'Company A' => [10 => 'A'], + 'Company B' => [20 => 'A'], + 'Company C' => [40 => 'A', 30 => 'A'], + ], + ], + 'newest disclosures first' => [ + 'newest-disclosures-first', + [ + 'Company B' => [20 => 'A'], + 'Company C' => [40 => 'A', 30 => 'A'], + 'Company A' => [10 => 'A'], + ], + ], + 'newest disclosures last' => [ + 'newest-disclosures-last', + [ + 'Company A' => [10 => 'A'], + 'Company C' => [30 => 'A', 40 => 'A'], + 'Company B' => [20 => 'A'], + ], + ], + 'newly added first' => [ + 'newly-added-first', + [ + 'Company A' => [10 => 'A'], + 'Company C' => [30 => 'A', 40 => 'A'], + 'Company B' => [20 => 'A'], + ], + ], + 'newly added last' => [ + 'newly-added-last', + [ + 'Company B' => [20 => 'A'], + 'Company C' => [40 => 'A', 30 => 'A'], + 'Company A' => [10 => 'A'], + ], + ], + ]; + } + + + /** + * @param array> $expected + * @dataProvider getExpected + */ + public function testSort(string $sort, array $expected): void + { + $companyA = new Company(1, 'Company A', null, 'company-a', 'company-a'); + $companyB = new Company(2, 'Company B', null, 'company-b', 'company-b'); + $companyC = new Company(3, 'Company C', null, 'company-c', 'company-c'); + $hashingAlgorithmA = new PasswordHashingAlgorithm(self::RANDOM_INT_ID, 'bcrypt', 'bcrypt', true, true); + $hashingAlgorithmB = new PasswordHashingAlgorithm(self::RANDOM_INT_ID, 'scrypt', 'scrypt', true, true); + $attributes = new StorageAlgorithmAttributes([], [], []); + $storageDisclosureA = new StorageDisclosure(self::RANDOM_INT_ID, 'https://disclosure-a.example', 'https://archive-a.example', null, new DateTime('6 days ago'), new DateTime('3 days ago'), 'docs', 'docs'); + $storageDisclosureB = new StorageDisclosure(self::RANDOM_INT_ID, 'https://disclosure-b.example', 'https://archive-b.example', null, new DateTime('3 days ago'), new DateTime('6 days ago'), 'docs', 'docs'); + $storageAlgorithmA = new StorageAlgorithm(self::RANDOM_STRING_ID, $hashingAlgorithmA, null, false, $attributes, null, $storageDisclosureA); + $storageAlgorithmB = new StorageAlgorithm(self::RANDOM_STRING_ID, $hashingAlgorithmB, null, false, $attributes, null, $storageDisclosureB); + $siteA = new StorageSpecificSite($this->rating, '10', 'https://site-a.example', 'site-a', [], $companyA, self::RANDOM_STRING_ID, $storageAlgorithmA); + $siteB = new StorageSpecificSite($this->rating, '20', 'https://site-b.example', 'site-b', [], $companyB, self::RANDOM_STRING_ID, $storageAlgorithmB); + $siteC = new StorageSpecificSite($this->rating, '30', 'https://site-c.example', 'site-c', [], $companyC, self::RANDOM_STRING_ID, $storageAlgorithmA); + $siteD = new StorageSpecificSite($this->rating, '40', 'https://d.example', 'site-d', [], $companyC, self::RANDOM_STRING_ID, $storageAlgorithmB); + $storageA = new Storage('100', self::RANDOM_INT_ID); + $storageA->addSite($siteA); + $storageB = new Storage('200', self::RANDOM_INT_ID); + $storageB->addSite($siteB); + $storageC = new Storage('300', self::RANDOM_INT_ID); + $storageC->addSite($siteC); + $storageD = new Storage('400', self::RANDOM_INT_ID); + $storageD->addSite($siteD); + $registry = new StorageRegistry(); + $registry->addCompany($companyA); + $registry->addCompany($companyC); + $registry->addCompany($companyB); + $registry->addSite($siteA); + $registry->addSite($siteC); + $registry->addSite($siteB); + $registry->addSite($siteD); + $registry->addStorage($storageA); + $registry->addStorage($storageC); + $registry->addStorage($storageB); + $registry->addStorage($storageD); + + $this->assertSorted($registry, $expected, $sort); + } + + + public function testGetDefaultSort(): void + { + Assert::same('a-z', $this->passwordsSorting->getDefaultSort()); + } + + + /** + * @param array> $expected + */ + private function assertSorted(StorageRegistry $registry, array $expected, string $sort): void + { + $this->passwordsSorting->sort($registry, $sort); + $actual = []; + foreach ($registry->getStorages() as $storage) { + foreach ($storage->getSites() as $site) { + $actual[$site->getCompany()->getCompanyName()][$site->getId()] = $site->getRating()->name; + } + } + Assert::same($expected, $actual); + } + +} + +TestCaseRunner::run(PasswordsSortingTest::class); From fe6552c3151ab4da6a2c44127e34154bfd17f211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Thu, 21 Nov 2024 03:02:31 +0100 Subject: [PATCH 09/24] Ensure $collator is Collator It would be enough to use ``` if (!$collator instanceof Collator) { ``` instead of just ``` if (!$collator) { ``` But why not remove `static $collator` as well. This fixes "Cannot call method getSortKey() on mixed." --- app/src/Pulse/Passwords/PasswordsSorting.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/Pulse/Passwords/PasswordsSorting.php b/app/src/Pulse/Passwords/PasswordsSorting.php index 5832bbba5..3295a020e 100644 --- a/app/src/Pulse/Passwords/PasswordsSorting.php +++ b/app/src/Pulse/Passwords/PasswordsSorting.php @@ -34,6 +34,14 @@ class PasswordsSorting self::NEWLY_ADDED_LAST => 'newly added last', ]; + private readonly Collator $collator; + + + public function __construct() + { + $this->collator = new Collator('en_US'); + } + public function sort(StorageRegistry $storages, string $sort): StorageRegistry { @@ -44,11 +52,7 @@ public function sort(StorageRegistry $storages, string $sort): StorageRegistry return $this->sortSites($storages, $a, $b, $sort, function (StorageRegistry $storages, StorageSite $siteA, StorageSite $siteB, string $sort): int { $result = $sort === self::RATING_A_F ? $siteA->getRating()->name <=> $siteB->getRating()->name : $siteB->getRating()->name <=> $siteA->getRating()->name; if ($result === 0) { - static $collator; - if (!$collator) { - $collator = new Collator('en_US'); - } - $result = $collator->getSortKey($storages->getCompany($siteA->getCompany()->getId())->getSortName()) <=> $collator->getSortKey($storages->getCompany($siteB->getCompany()->getId())->getSortName()); + $result = $this->collator->getSortKey($storages->getCompany($siteA->getCompany()->getId())->getSortName()) <=> $this->collator->getSortKey($storages->getCompany($siteB->getCompany()->getId())->getSortName()); if ($result === 0) { $subKeyA = $siteA instanceof StorageSpecificSite ? $siteA->getUrl() : $siteA->getLatestAlgorithm()->getAlias(); $subKeyB = $siteB instanceof StorageSpecificSite ? $siteB->getUrl() : $siteB->getLatestAlgorithm()->getAlias(); From f0bde6022e058bfad4e049cd2819b04a675bc698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Thu, 21 Nov 2024 03:43:54 +0100 Subject: [PATCH 10/24] Adding docblock --- app/src/Pulse/Passwords/PasswordsSorting.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/Pulse/Passwords/PasswordsSorting.php b/app/src/Pulse/Passwords/PasswordsSorting.php index 3295a020e..4415930be 100644 --- a/app/src/Pulse/Passwords/PasswordsSorting.php +++ b/app/src/Pulse/Passwords/PasswordsSorting.php @@ -91,6 +91,9 @@ public function sort(StorageRegistry $storages, string $sort): StorageRegistry } + /** + * @param callable(StorageRegistry, StorageSite, StorageSite, string): int $callback + */ private function sortSites(StorageRegistry $storages, Storage $a, Storage $b, string $sort, callable $callback): int { if (count($a->getSites()) > 1 || count($b->getSites()) > 1) { From 213a6ec7e4ca84a3e5e95b822eaf42e9ee30a2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Wed, 27 Nov 2024 02:34:32 +0100 Subject: [PATCH 11/24] Add assert()s to Passwords::addStorage() --- app/src/Pulse/Companies.php | 5 +- .../PasswordHashingDisclosures.php | 13 +- app/src/Pulse/Passwords/Passwords.php | 65 ++++- app/src/Pulse/Sites.php | 5 +- app/tests/Pulse/Passwords/PasswordsTest.phpt | 244 ++++++++++++++++++ 5 files changed, 307 insertions(+), 25 deletions(-) diff --git a/app/src/Pulse/Companies.php b/app/src/Pulse/Companies.php index 75bb81142..bbf31c7a4 100644 --- a/app/src/Pulse/Companies.php +++ b/app/src/Pulse/Companies.php @@ -3,8 +3,8 @@ namespace MichalSpacekCz\Pulse; -use DateTime; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use Nette\Database\Explorer; readonly class Companies @@ -13,6 +13,7 @@ public function __construct( private Explorer $database, private TypedDatabase $typedDatabase, + private DateTimeFactory $dateTimeFactory, ) { } @@ -82,7 +83,7 @@ public function add(string $name, string $tradeName, string $alias): int 'name' => $name, 'trade_name' => (empty($tradeName) ? null : $tradeName), 'alias' => $alias, - 'added' => new DateTime(), + 'added' => $this->dateTimeFactory->create(), ]); return (int)$this->database->getInsertId(); } diff --git a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php index 57c8ee37a..904cb719c 100644 --- a/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php +++ b/app/src/Pulse/Passwords/Disclosures/PasswordHashingDisclosures.php @@ -3,8 +3,8 @@ namespace MichalSpacekCz\Pulse\Passwords\Disclosures; -use DateTime; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use MichalSpacekCz\Pulse\Passwords\Rating; use Nette\Database\Explorer; @@ -15,6 +15,7 @@ public function __construct( private Explorer $database, private TypedDatabase $typedDatabase, private Rating $rating, + private DateTimeFactory $dateTimeFactory, ) { } @@ -62,11 +63,7 @@ public function getInvisibleDisclosures(): array public function getDisclosureId(string $url, string $archive): ?int { - $id = $this->typedDatabase->fetchFieldIntNullable('SELECT id FROM password_disclosures WHERE url = ? AND archive = ?', $url, $archive); - if ($id === null) { - return null; - } - return $id; + return $this->typedDatabase->fetchFieldIntNullable('SELECT id FROM password_disclosures WHERE url = ? AND archive = ?', $url, $archive); } @@ -80,8 +77,8 @@ public function addDisclosure(int $type, string $url, string $archive, string $n 'url' => $url, 'archive' => $archive, 'note' => (empty($note) ? null : $note), - 'published' => (empty($published) ? null : new DateTime($published)), - 'added' => new DateTime(), + 'published' => (empty($published) ? null : $this->dateTimeFactory->create($published)), + 'added' => $this->dateTimeFactory->create(), ]); return (int)$this->database->getInsertId(); } diff --git a/app/src/Pulse/Passwords/Passwords.php b/app/src/Pulse/Passwords/Passwords.php index 72f9f4a34..a4ba814b3 100644 --- a/app/src/Pulse/Passwords/Passwords.php +++ b/app/src/Pulse/Passwords/Passwords.php @@ -240,7 +240,7 @@ public function getStoragesByCompanyId(int $companyId): StorageRegistry /** * Get storage id by company id, algorithm id, site id. */ - private function getStorageId(int $companyId, int $algoId, string $siteId, string $from, bool $fromConfirmed, ?string $attributes, ?string $note): ?int + private function getStorageId(int $companyId, int $algoId, string $siteId, string $from, bool $fromConfirmed, string $attributes, string $note): ?int { $result = $this->typedDatabase->fetchFieldIntNullable( 'SELECT id FROM password_storages WHERE ?', @@ -250,8 +250,8 @@ private function getStorageId(int $companyId, int $algoId, string $siteId, strin 'key_sites' => ($siteId === Sites::ALL ? null : $siteId), 'from' => $from !== '' ? new DateTime($from) : null, 'from_confirmed' => $fromConfirmed, - 'attributes' => $attributes !== null && $attributes !== '' ? $attributes : null, - 'note' => ($note !== null && $note !== '') ? $note : null, + 'attributes' => $attributes !== '' ? $attributes : null, + 'note' => $note !== '' ? $note : null, ], ); @@ -302,21 +302,60 @@ private function pairDisclosureStorage(int $disclosureId, int $storageId): void */ public function addStorage(ArrayHash $values): bool { - /** @var ArrayHash $newCompany */ - $newCompany = $values->company->new; - /** @var ArrayHash $newSite */ - $newSite = $values->site->new; - /** @var ArrayHash $newAlgo */ - $newAlgo = $values->algo->new; + assert($values->company instanceof ArrayHash); + assert(is_int($values->company->id) || $values->company->id === null); + assert($values->company->new instanceof ArrayHash); + assert(is_string($values->company->new->name)); + assert(is_string($values->company->new->dba)); + assert(is_string($values->company->new->alias)); + assert($values->site instanceof ArrayHash); + assert(is_int($values->site->id) || $values->site->id === Sites::ALL || $values->site->id === null); + assert($values->site->new instanceof ArrayHash); + assert(is_string($values->site->new->url)); + assert(is_string($values->site->new->alias)); + assert(is_string($values->site->new->sharedWith)); + assert($values->algo instanceof ArrayHash); + assert(is_int($values->algo->id) || $values->algo->id === null); + assert($values->algo->new instanceof ArrayHash); + assert(is_string($values->algo->new->algoName)); + assert(is_string($values->algo->new->alias)); + assert(is_bool($values->algo->new->salted)); + assert(is_bool($values->algo->new->stretched)); + assert(is_string($values->algo->from)); + assert(is_bool($values->algo->fromConfirmed)); + assert(is_string($values->algo->attributes)); + assert(is_string($values->algo->note)); + assert($values->disclosure instanceof ArrayHash); + assert($values->disclosure->new instanceof ArrayHash); $this->database->beginTransaction(); - $companyId = (empty($newCompany->name) ? (int)$values->company->id : $this->companies->add($newCompany->name, $newCompany->dba, $newCompany->alias)); - $siteId = (string)(empty($newSite->url) + $companyId = $values->company->new->name === '' && $values->company->id !== null ? $values->company->id : $this->companies->add( + $values->company->new->name, + $values->company->new->dba, + $values->company->new->alias, + ); + $siteId = (string)($values->site->new->url === '' ? $values->site->id // the value can also be "all" - : $this->sites->add($newSite->url, $newSite->alias, $newSite->sharedWith, $companyId) + : $this->sites->add( + $values->site->new->url, + $values->site->new->alias, + $values->site->new->sharedWith, + $companyId, + ) + ); + $algoId = $values->algo->new->algoName === '' && $values->algo->id !== null ? $values->algo->id : $this->hashingAlgorithms->addAlgorithm( + $values->algo->new->algoName, + $values->algo->new->alias, + $values->algo->new->salted, + $values->algo->new->stretched, ); - $algoId = (empty($newAlgo->algoName) ? (int)$values->algo->id : $this->hashingAlgorithms->addAlgorithm($newAlgo->algoName, $newAlgo->alias, $newAlgo->salted, $newAlgo->stretched)); foreach ($values->disclosure->new as $disclosure) { + assert($disclosure instanceof ArrayHash); + assert(is_int($disclosure->disclosureType)); + assert(is_string($disclosure->url)); + assert(is_string($disclosure->archive)); + assert(is_string($disclosure->note)); + assert(is_string($disclosure->published)); if ($disclosure->url) { $disclosureId = $this->passwordHashingDisclosures->getDisclosureId($disclosure->url, $disclosure->archive); if ($disclosureId === null) { diff --git a/app/src/Pulse/Sites.php b/app/src/Pulse/Sites.php index 5a70413c0..91be5d970 100644 --- a/app/src/Pulse/Sites.php +++ b/app/src/Pulse/Sites.php @@ -3,8 +3,8 @@ namespace MichalSpacekCz\Pulse; -use DateTime; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use Nette\Database\Explorer; readonly class Sites @@ -16,6 +16,7 @@ public function __construct( private Explorer $database, private TypedDatabase $typedDatabase, + private DateTimeFactory $dateTimeFactory, ) { } @@ -63,7 +64,7 @@ public function add(string $url, string $alias, string $sharedWith, int $company 'alias' => $alias, 'shared_with' => $sharedWith ?: null, 'key_companies' => $companyId, - 'added' => new DateTime(), + 'added' => $this->dateTimeFactory->create(), ]); return (int)$this->database->getInsertId(); } diff --git a/app/tests/Pulse/Passwords/PasswordsTest.phpt b/app/tests/Pulse/Passwords/PasswordsTest.phpt index 9ed4eea86..b10b914f9 100644 --- a/app/tests/Pulse/Passwords/PasswordsTest.phpt +++ b/app/tests/Pulse/Passwords/PasswordsTest.phpt @@ -5,10 +5,18 @@ declare(strict_types = 1); namespace MichalSpacekCz\Pulse\Passwords; use DateTime; +use DateTimeImmutable; +use MichalSpacekCz\Form\Pulse\PasswordsStorageAlgorithmFormFactory; +use MichalSpacekCz\Form\UiForm; use MichalSpacekCz\Pulse\Passwords\Storage\StorageSpecificSite; use MichalSpacekCz\Pulse\Passwords\Storage\StorageWildcardSite; +use MichalSpacekCz\Test\Application\ApplicationPresenter; use MichalSpacekCz\Test\Database\Database; +use MichalSpacekCz\Test\DateTime\DateTimeMachineFactory; +use MichalSpacekCz\Test\PrivateProperty; use MichalSpacekCz\Test\TestCaseRunner; +use Nette\Application\Application; +use Override; use Tester\Assert; use Tester\TestCase; @@ -21,10 +29,21 @@ class PasswordsTest extends TestCase public function __construct( private readonly Passwords $passwords, private readonly Database $database, + private readonly Application $application, + private readonly ApplicationPresenter $applicationPresenter, + private readonly PasswordsStorageAlgorithmFormFactory $formFactory, + private readonly DateTimeMachineFactory $dateTimeFactory, ) { } + #[Override] + protected function tearDown(): void + { + $this->database->reset(); + } + + public function testGetAllStorages(): void { $this->database->setFetchAllDefaultResult([ @@ -346,6 +365,231 @@ class PasswordsTest extends TestCase } } + + public function testAddStorage(): void + { + // Company + $this->database->addFetchAllResult([ + [ + 'id' => 1, + 'name' => 'Slevomat.cz, s.r.o.', + 'tradeName' => null, + 'alias' => 'slevomat.cz', + 'sortName' => 'Slevomat.cz, s.r.o.', + ], + ]); + // Site + $this->database->addFetchAllResult([ + [ + 'id' => 2, + 'url' => 'https://site.example', + 'alias' => 'site.example', + ], + ]); + // Algorithm + $this->database->addFetchAllResult([ + [ + 'id' => 3, + 'algo' => 'bcrypt', + 'alias' => 'bcrypt', + 'salted' => 1, + 'stretched' => 1, + ], + ]); + // Disclosure types + $this->database->addFetchAllResult([ + [ + 'id' => 4, + 'type' => 'Twitter', + 'alias' => 'twitter', + ], + ]); + // Disclosure id + $this->database->addFetchFieldResult(5); + // Storage id + $this->database->addFetchFieldResult(6); + + $form = $this->getForm(); + $form->setDefaults([ + 'company' => [ + 'id' => '1', + ], + 'site' => [ + 'id' => '2', + ], + 'algo' => [ + 'id' => '3', + ], + 'disclosure' => [ + 'new' => [ + [ + 'url' => 'https://disclosure.example/', + 'archive' => 'https://archive.disclosure.example/', + 'disclosureType' => '4', + 'note' => 'note', + 'published' => '2020-11-22', + ], + ], + ], + ]); + + $this->passwords->addStorage($form->getFormValues()); + Assert::same([], $this->database->getParamsArrayForQuery('INSERT INTO companies')); + Assert::same( + [['key_password_disclosures' => 5, 'key_password_storages' => 6]], + $this->database->getParamsArrayForQuery('INSERT INTO password_disclosures_password_storages'), + 'Passwords::pairDisclosureStorage() result not as expected', + ); + } + + + public function testAddStorageNewItems(): void + { + // Company + $this->database->addFetchAllResult([]); + // Site + $this->database->addFetchAllResult([]); + // Algorithm + $this->database->addFetchAllResult([]); + // Disclosure types + $this->database->addFetchAllResult([ + [ + 'id' => 4, + 'type' => 'Twitter', + 'alias' => 'twitter', + ], + ]); + // companies id + $this->database->addInsertId('5'); + // sites id + $this->database->addInsertId('6'); + // password_algos id + $this->database->addInsertId('7'); + // password_disclosures id + $this->database->addInsertId('8'); + // password_storages id + $this->database->addInsertId('9'); + + $form = $this->getForm(); + $form->setDefaults([ + 'company' => [ + 'new' => [ + 'name' => 'Slevomat.cz, s.r.o.', + 'dba' => '', + 'alias' => 'slevomat.cz', + ], + 'id' => null, + ], + 'site' => [ + 'new' => [ + 'url' => 'https://sl.example', + 'alias' => 'sl.example', + 'sharedWith' => '', + ], + 'id' => null, + ], + 'algo' => [ + 'new' => [ + 'algoName' => 'JavasCrypt', + 'alias' => 'javascrypt', + 'salted' => true, + 'stretched' => true, + ], + 'id' => null, + 'from' => '2001-02-03 04:05:06', + 'fromConfirmed' => true, + 'attributes' => '{"foo":"bar"}', + 'note' => 'algo note', + ], + 'disclosure' => [ + 'new' => [ + [ + 'url' => 'https://di.example/', + 'archive' => 'https://ar.di.example/', + 'disclosureType' => '4', + 'note' => 'note', + 'published' => '2020-11-22', + ], + ], + ], + ]); + $this->dateTimeFactory->setDateTime(new DateTimeImmutable('2020-01-01 12:34:56')); + + $this->passwords->addStorage($form->getFormValues()); + Assert::same( + [[ + 'name' => 'Slevomat.cz, s.r.o.', + 'trade_name' => null, + 'alias' => 'slevomat.cz', + 'added' => '2020-01-01 12:34:56', + ]], + $this->database->getParamsArrayForQuery('INSERT INTO companies'), + ); + Assert::same( + [[ + 'url' => 'https://sl.example', + 'alias' => 'sl.example', + 'shared_with' => null, + 'key_companies' => 5, + 'added' => '2020-01-01 12:34:56', + ]], + $this->database->getParamsArrayForQuery('INSERT INTO sites'), + ); + Assert::same( + [[ + 'algo' => 'JavasCrypt', + 'alias' => 'javascrypt', + 'salted' => true, + 'stretched' => true, + ]], + $this->database->getParamsArrayForQuery('INSERT INTO password_algos'), + ); + Assert::same( + [[ + 'key_password_disclosure_types' => 4, + 'url' => 'https://di.example/', + 'archive' => 'https://ar.di.example/', + 'note' => 'note', + 'published' => '2020-01-01 12:34:56', + 'added' => '2020-01-01 12:34:56', + ]], + $this->database->getParamsArrayForQuery('INSERT INTO password_disclosures'), + ); + Assert::same( + [[ + 'key_companies' => null, + 'key_password_algos' => 7, + 'key_sites' => 6, + 'from' => '2001-02-03 04:05:06', + 'from_confirmed' => true, + 'attributes' => '{"foo":"bar"}', + 'note' => 'algo note', + ]], + $this->database->getParamsArrayForQuery('INSERT INTO password_storages'), + ); + Assert::same( + [['key_password_disclosures' => 8, 'key_password_storages' => 9]], + $this->database->getParamsArrayForQuery('INSERT INTO password_disclosures_password_storages'), + 'Passwords::pairDisclosureStorage() result not as expected', + ); + } + + + private function getForm(): UiForm + { + $form = $this->formFactory->create( + function (): void { + // This won't be called in this test anyway + }, + 1, + ); + $presenter = $this->applicationPresenter->createUiPresenter('Admin:Pulse', 'foo', 'slides'); + PrivateProperty::setValue($this->application, 'presenter', $presenter); + /** @noinspection PhpInternalEntityUsedInspection */ + $form->setParent($presenter); + return $form; + } + } TestCaseRunner::run(PasswordsTest::class); From ad3180b50f7c16f0f931cd2e001b05aeccd255ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 29 Nov 2024 04:01:36 +0100 Subject: [PATCH 12/24] Slide filenames are always a string, the database column is not nullable --- app/tests/Talks/Slides/TalkSlidesTest.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/Talks/Slides/TalkSlidesTest.phpt b/app/tests/Talks/Slides/TalkSlidesTest.phpt index bc4c33cc3..4608cb1f2 100644 --- a/app/tests/Talks/Slides/TalkSlidesTest.phpt +++ b/app/tests/Talks/Slides/TalkSlidesTest.phpt @@ -126,7 +126,7 @@ class TalkSlidesTest extends TestCase 'alias' => 'alias-2', 'number' => 4, 'filename' => 'filename2.jpg', - 'filenameAlternative' => null, + 'filenameAlternative' => '', 'title' => 'Title 2', 'speakerNotesTexy' => 'speaker **notes** 2', ], From 2fc852ba9359b1383b238984b721605ec42f6c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 29 Nov 2024 04:02:14 +0100 Subject: [PATCH 13/24] Splitting a string, even if empty, will always return an array --- app/src/Formatter/TexyPhraseHandler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/Formatter/TexyPhraseHandler.php b/app/src/Formatter/TexyPhraseHandler.php index 955a1e0a5..03fe0097b 100644 --- a/app/src/Formatter/TexyPhraseHandler.php +++ b/app/src/Formatter/TexyPhraseHandler.php @@ -110,6 +110,9 @@ public function solve(HandlerInvocation $invocation, string $phrase, string $con private function getLink(string $url, string $locale): string { $args = Preg::split('/[\s,]+/', $url); + if ($args === []) { + throw new ShouldNotHappenException('Preg::split() should always return a non-empty array'); + } $action = array_shift($args); if (Arrays::contains([self::TRAINING_ACTION, self::COMPANY_TRAINING_ACTION], $action)) { $args = [$this->trainingLocales->getLocaleActions($args[0])[$locale]]; From 6b1dd08e2d56c022d097d10bac118754940a14b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 29 Nov 2024 04:02:51 +0100 Subject: [PATCH 14/24] Add assert() tests to narrow types and add tests --- app/src/Articles/Blog/BlogPostEdits.php | 9 +- app/src/Articles/Blog/BlogPostLocaleUrls.php | 6 ++ .../Articles/Blog/BlogPostEditsTest.phpt | 45 +++++++++ .../Articles/Blog/BlogPostLocaleUrlsTest.phpt | 94 +++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 app/tests/Articles/Blog/BlogPostEditsTest.phpt create mode 100644 app/tests/Articles/Blog/BlogPostLocaleUrlsTest.phpt diff --git a/app/src/Articles/Blog/BlogPostEdits.php b/app/src/Articles/Blog/BlogPostEdits.php index 747b12d5e..fbe8e2d59 100644 --- a/app/src/Articles/Blog/BlogPostEdits.php +++ b/app/src/Articles/Blog/BlogPostEdits.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Articles\Blog; +use DateTime; use MichalSpacekCz\Articles\ArticleEdit; use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\DateTime\DateTimeZoneFactory; @@ -35,9 +36,11 @@ public function getEdits(int $postId): array ORDER BY edited_at DESC'; $edits = []; foreach ($this->typedDatabase->fetchAll($sql, $postId) as $row) { - $editedAt = $row->editedAt; - $editedAt->setTimezone($this->dateTimeZoneFactory->get($row->editedAtTimezone)); - $edits[] = new ArticleEdit($editedAt, $this->texyFormatter->format($row->summaryTexy), $row->summaryTexy); + assert($row->editedAt instanceof DateTime); + assert(is_string($row->editedAtTimezone)); + assert(is_string($row->summaryTexy)); + $row->editedAt->setTimezone($this->dateTimeZoneFactory->get($row->editedAtTimezone)); + $edits[] = new ArticleEdit($row->editedAt, $this->texyFormatter->format($row->summaryTexy), $row->summaryTexy); } return $edits; } diff --git a/app/src/Articles/Blog/BlogPostLocaleUrls.php b/app/src/Articles/Blog/BlogPostLocaleUrls.php index c97b97e62..dc8156cdd 100644 --- a/app/src/Articles/Blog/BlogPostLocaleUrls.php +++ b/app/src/Articles/Blog/BlogPostLocaleUrls.php @@ -3,6 +3,7 @@ namespace MichalSpacekCz\Articles\Blog; +use DateTime; use MichalSpacekCz\Database\TypedDatabase; use MichalSpacekCz\Tags\Tags; use Nette\Utils\JsonException; @@ -40,6 +41,11 @@ public function get(string $slug): array OR bp.slug = ? ORDER BY l.id_locale'; foreach ($this->typedDatabase->fetchAll($sql, $slug, $slug) as $row) { + assert(is_string($row->locale)); + assert(is_string($row->slug)); + assert($row->published instanceof DateTime || $row->published === null); + assert(is_string($row->previewKey) || $row->previewKey === null); + assert(is_string($row->slugTags) || $row->slugTags === null); $post = new BlogPostLocaleUrl( $row->locale, $row->slug, diff --git a/app/tests/Articles/Blog/BlogPostEditsTest.phpt b/app/tests/Articles/Blog/BlogPostEditsTest.phpt new file mode 100644 index 000000000..cc298d3ee --- /dev/null +++ b/app/tests/Articles/Blog/BlogPostEditsTest.phpt @@ -0,0 +1,45 @@ +database->setFetchAllDefaultResult([ + [ + 'editedAt' => new DateTime('2020-05-06 07:08:09'), + 'editedAtTimezone' => 'Europe/Tallinn', + 'summaryTexy' => '**Summary**', + ], + ]); + $edits = $this->blogPostEdits->getEdits(123); + Assert::count(1, $edits); + Assert::same('2020-05-06T08:08:09.000000+03:00', $edits[0]->getEditedAt()->format(DateTimeFormat::RFC3339_MICROSECONDS)); + Assert::same('**Summary**', $edits[0]->getSummaryTexy()); + Assert::same('Summary', $edits[0]->getSummary()->toHtml()); + } + +} + +TestCaseRunner::run(BlogPostEditsTest::class); diff --git a/app/tests/Articles/Blog/BlogPostLocaleUrlsTest.phpt b/app/tests/Articles/Blog/BlogPostLocaleUrlsTest.phpt new file mode 100644 index 000000000..4839e233e --- /dev/null +++ b/app/tests/Articles/Blog/BlogPostLocaleUrlsTest.phpt @@ -0,0 +1,94 @@ +database->setFetchAllDefaultResult([ + [ + 'locale' => 'cs_CZ', + 'slug' => 'le-blog-post', + 'published' => new DateTime('2020-05-06 07:08:09'), + 'previewKey' => 'rand0m', + 'slugTags' => '["foo","bar"]', + ], + ]); + $urls = $this->blogPostLocaleUrls->get('le-blog-post'); + Assert::count(1, $urls); + Assert::same('2020-05-06T07:08:09.000000+02:00', $urls[0]->getPublished()?->format(DateTimeFormat::RFC3339_MICROSECONDS)); + Assert::same('cs_CZ', $urls[0]->getLocale()); + Assert::null($urls[0]->getPreviewKey()); + Assert::same('le-blog-post', $urls[0]->getSlug()); + Assert::same(['foo', 'bar'], $urls[0]->getSlugTags()); + } + + + /** + * @return array + */ + public function getPublished(): array + { + return [ + 'no published' => [ + null, + true, + ], + 'future' => [ + '7 days + 1 week', // Remember B.B.E.? + true, + ], + 'past' => [ + '-7 years -50 days', // Or Groove Coverage? + false, + ], + ]; + } + + + /** + * @dataProvider getPublished + */ + public function testGetPreviewKey(?string $published, bool $hasPreviewKey): void + { + $this->database->setFetchAllDefaultResult([ + [ + 'locale' => 'cs_CZ', + 'slug' => 'le-blog-post', + 'published' => $published !== null ? new DateTime($published) : null, + 'previewKey' => 'rand0m', + 'slugTags' => null, + ], + ]); + $urls = $this->blogPostLocaleUrls->get('le-blog-post'); + if ($hasPreviewKey) { + Assert::same('rand0m', $urls[0]->getPreviewKey()); + } else { + Assert::null($urls[0]->getPreviewKey()); + } + } + +} + +TestCaseRunner::run(BlogPostLocaleUrlsTest::class); From 0ac8391f0c1d967551875c74a997a04f569c4e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 18 Nov 2024 05:54:06 +0100 Subject: [PATCH 15/24] Update Psalm's baseline --- app/psalm-baseline.xml | 128 ----------------------------------------- 1 file changed, 128 deletions(-) diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index a92affeed..4831575e4 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -1,27 +1,5 @@ - - - slugTags]]> - tags]]> - - - - - - editedAtTimezone]]> - summaryTexy]]> - - - - - locale]]> - previewKey]]> - published]]> - slug]]> - slugTags]]> - - ico]]> @@ -293,11 +271,6 @@ - - - - - videoThumbnail]]> @@ -316,77 +289,15 @@ - - - alias]]> - id]]> - name]]> - sortName]]> - tradeName]]> - - - - - algo]]> - alias]]> - id]]> - - - - - alias]]> - id]]> - type]]> - - - - - archive]]> - disclosureType]]> - note]]> - published]]> - url]]> - algoName]]> - alias]]> - salted]]> - stretched]]> - alias]]> - dba]]> - name]]> - alias]]> - sharedWith]]> - url]]> - algo->attributes]]> - algo->from]]> - algo->fromConfirmed]]> - algo->note]]> - - - - - alias]]> - id]]> - url]]> - - - - - - filename ?? '']]> filenameAlternative ?? '']]> - alias]]> - id]]> - number]]> - speakerNotesTexy]]> - title]]> alias]]> number]]> number]]> @@ -398,25 +309,11 @@ title]]> - - - - - - - - company ?? self::FIELD_MISSING_VALUE]]> - companyId ?? self::FIELD_MISSING_VALUE]]> - companyTaxId ?? self::FIELD_MISSING_VALUE]]> - name ?? self::FIELD_MISSING_VALUE]]> - note ?? '']]> - - city]]> @@ -452,26 +349,6 @@ zip]]> - - - id]]> - status]]> - statusId]]> - statusTime]]> - statusTimeTimeZone]]> - - - - - status]]> - - - - - description]]> - href]]> - - ranking]]> @@ -483,11 +360,6 @@ - - - mac]]> - - entry]]> From f79ba268a78ed48aeb2c8c35e1c917a0bb61c269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 29 Nov 2024 05:14:26 +0100 Subject: [PATCH 16/24] Use Texy constants on the class, not on the object --- app/psalm-baseline.xml | 5 ----- app/src/Formatter/TexyFormatter.php | 2 +- app/src/Formatter/TexyPhraseHandler.php | 3 ++- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 4831575e4..6884fe5a6 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -266,11 +266,6 @@ - - - - - videoThumbnail]]> diff --git a/app/src/Formatter/TexyFormatter.php b/app/src/Formatter/TexyFormatter.php index b2b771048..a5b6b19a6 100644 --- a/app/src/Formatter/TexyFormatter.php +++ b/app/src/Formatter/TexyFormatter.php @@ -105,7 +105,7 @@ public function setTopHeading(int $level): self public function getTexy(): Texy { $this->texy = new Texy(); - $this->texy->allowedTags = $this->texy::NONE; + $this->texy->allowedTags = Texy::NONE; $this->texy->imageModule->root = "{$this->staticRoot}/{$this->imagesRoot}"; $this->texy->imageModule->fileRoot = "{$this->locationRoot}/{$this->imagesRoot}"; $this->texy->figureModule->widthDelta = false; // prevents adding 'unsafe-inline' style="width: Xpx" attribute to
diff --git a/app/src/Formatter/TexyPhraseHandler.php b/app/src/Formatter/TexyPhraseHandler.php index 03fe0097b..61c47b6b5 100644 --- a/app/src/Formatter/TexyPhraseHandler.php +++ b/app/src/Formatter/TexyPhraseHandler.php @@ -21,6 +21,7 @@ use Texy\HtmlElement; use Texy\Link; use Texy\Modifier; +use Texy\Texy; readonly class TexyPhraseHandler { @@ -98,7 +99,7 @@ public function solve(HandlerInvocation $invocation, string $phrase, string $con $trainingLink = $this->proceed($invocation, $phrase, $content, $modifier, $link); if ($trainingLink !== false) { $el->add($trainingLink); - $el->add($texy->protect($this->getTrainingSuffix($name), $texy::CONTENT_TEXTUAL)); + $el->add($texy->protect($this->getTrainingSuffix($name), Texy::CONTENT_TEXTUAL)); return $el; } } From 5134c442d526ebc439a147abeec712129021cff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sat, 30 Nov 2024 02:25:04 +0100 Subject: [PATCH 17/24] Add assert()s to narrow types in (some) Forms --- app/config/tests.neon | 2 + app/psalm-baseline.xml | 10 -- app/psalm.xml | 2 +- app/src/Form/SignInFormFactory.php | 4 +- app/src/Form/TrainingFileFormFactory.php | 2 + app/src/Form/UpcKeysSsidFormFactory.php | 1 + app/src/Test/Form/FormComponents.php | 19 ++++ app/src/Test/Security/NullUserStorage.php | 50 +++++++++ app/tests/Form/SignInFormFactoryTest.phpt | 106 ++++++++++++++++++ .../Form/SignInHoneypotFormFactoryTest.phpt | 15 +-- app/tests/Form/TalkSlidesFormFactoryTest.phpt | 2 +- .../Form/TrainingFileFormFactoryTest.phpt | 53 +++++++++ .../Form/UpcKeysSsidFormFactoryTest.phpt | 50 +++++++++ 13 files changed, 292 insertions(+), 24 deletions(-) create mode 100644 app/src/Test/Form/FormComponents.php create mode 100644 app/src/Test/Security/NullUserStorage.php create mode 100644 app/tests/Form/SignInFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingFileFormFactoryTest.phpt create mode 100644 app/tests/Form/UpcKeysSsidFormFactoryTest.phpt diff --git a/app/config/tests.neon b/app/config/tests.neon index e480d9c7e..8f97b32fc 100644 --- a/app/config/tests.neon +++ b/app/config/tests.neon @@ -43,6 +43,7 @@ services: database.pulse.explorer: @database.default.explorer dateTimeFactory: MichalSpacekCz\Test\DateTime\DateTimeMachineFactory httpClient: MichalSpacekCz\Test\Http\Client\HttpClientMock + - MichalSpacekCz\Test\Form\FormComponents session.session: MichalSpacekCz\Test\Http\NullSession http.request: MichalSpacekCz\Test\Http\Request http.response: MichalSpacekCz\Test\Http\Response @@ -50,6 +51,7 @@ services: mail.mailer: MichalSpacekCz\Test\NullMailer translation.translator: MichalSpacekCz\Test\NoOpTranslator(availableLocales: [cs_CZ, en_US], defaultLocale: cs_CZ) tracy.logger: MichalSpacekCz\Test\NullLogger + security.userStorage: MichalSpacekCz\Test\Security\NullUserStorage - MichalSpacekCz\Test\TestCaseRunner trainingFilesStorage: MichalSpacekCz\Test\Training\TrainingFilesNullStorage cache.storage: Nette\Caching\Storages\DevNullStorage diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 6884fe5a6..77a44a36b 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -71,11 +71,6 @@ site->new->url]]> - - - password]]> - - action]]> @@ -212,11 +207,6 @@ videoHref]]> - - - file]]> - - invoice]]> diff --git a/app/psalm.xml b/app/psalm.xml index 10575a7eb..fd368729b 100644 --- a/app/psalm.xml +++ b/app/psalm.xml @@ -113,7 +113,7 @@ - + diff --git a/app/src/Form/SignInFormFactory.php b/app/src/Form/SignInFormFactory.php index 27374707b..b5311c261 100644 --- a/app/src/Form/SignInFormFactory.php +++ b/app/src/Form/SignInFormFactory.php @@ -32,9 +32,11 @@ public function create(callable $onSuccess): UiForm $this->controlsFactory->addSignIn($form); $form->onSuccess[] = function (UiForm $form) use ($onSuccess): void { $values = $form->getFormValues(); + assert(is_string($values->username)); + assert(is_string($values->password)); $this->user->setExpiration('30 minutes', true); try { - $this->user->login((string)$values->username, $values->password); + $this->user->login($values->username, $values->password); Debugger::log("Successful sign-in attempt ({$values->username}, {$this->httpRequest->getRemoteAddress()})", 'auth'); if ($values->remember) { $this->authenticator->storePermanentLogin($this->user); diff --git a/app/src/Form/TrainingFileFormFactory.php b/app/src/Form/TrainingFileFormFactory.php index 39f030955..de6296a60 100644 --- a/app/src/Form/TrainingFileFormFactory.php +++ b/app/src/Form/TrainingFileFormFactory.php @@ -5,6 +5,7 @@ use DateTimeInterface; use MichalSpacekCz\Training\Files\TrainingFiles; +use Nette\Http\FileUpload; use Nette\Utils\Html; readonly class TrainingFileFormFactory @@ -28,6 +29,7 @@ public function create(callable $onSuccess, DateTimeInterface $trainingStart, ar $form->addSubmit('submit', 'Přidat'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess, $trainingStart, $applicationIdsAllowedFiles): void { $values = $form->getFormValues(); + assert($values->file instanceof FileUpload); if ($values->file->isOk()) { $filename = $this->trainingFiles->addFile($trainingStart, $values->file, $applicationIdsAllowedFiles); $message = Html::el()->setText('Soubor ') diff --git a/app/src/Form/UpcKeysSsidFormFactory.php b/app/src/Form/UpcKeysSsidFormFactory.php index b0cb96247..6933b9f0e 100644 --- a/app/src/Form/UpcKeysSsidFormFactory.php +++ b/app/src/Form/UpcKeysSsidFormFactory.php @@ -32,6 +32,7 @@ public function create(callable $onSuccess, ?string $ssid): UiForm ->setHtmlAttribute('data-alt', 'Wait…'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess): void { $values = $form->getFormValues(); + assert(is_string($values->ssid)); $ssid = strtoupper(trim($values->ssid)); $onSuccess($ssid); }; diff --git a/app/src/Test/Form/FormComponents.php b/app/src/Test/Form/FormComponents.php new file mode 100644 index 000000000..590ffac66 --- /dev/null +++ b/app/src/Test/Form/FormComponents.php @@ -0,0 +1,19 @@ +getComponent($component); + assert($field instanceof TextInput); + $field->setDefaultValue($value); + } + +} diff --git a/app/src/Test/Security/NullUserStorage.php b/app/src/Test/Security/NullUserStorage.php new file mode 100644 index 000000000..0f994d99a --- /dev/null +++ b/app/src/Test/Security/NullUserStorage.php @@ -0,0 +1,50 @@ +authenticated = true; + $this->identity = $identity; + } + + + #[Override] + public function clearAuthentication(bool $clearIdentity): void + { + $this->authenticated = true; + $this->reason = User::LogoutManual; + if ($clearIdentity === true) { + $this->identity = null; + } + } + + + #[Override] + public function getState(): array + { + return [$this->authenticated, $this->identity, $this->reason]; + } + + + #[Override] + public function setExpiration(?string $expire, bool $clearIdentity): void + { + } + +} diff --git a/app/tests/Form/SignInFormFactoryTest.phpt b/app/tests/Form/SignInFormFactoryTest.phpt new file mode 100644 index 000000000..73cdf34dc --- /dev/null +++ b/app/tests/Form/SignInFormFactoryTest.phpt @@ -0,0 +1,106 @@ +setRemoteAddress('127.31.33.7'); + $this->form = $formFactory->create(function () { + }); + $presenter = $applicationPresenter->createUiPresenter('Admin:Sign', 'foo', 'in'); + /** @noinspection PhpInternalEntityUsedInspection */ + $this->form->setParent($presenter); + + $service = $container->getService('passwordEncryption'); + assert($service instanceof SymmetricKeyEncryption); + $this->passwordEncryption = $service; + } + + + #[Override] + protected function tearDown(): void + { + $this->logger->reset(); + $this->form->cleanErrors(); + } + + + /** + * @return list + */ + public function getCredentials(): array + { + return [ + ['root', 'hunter2', null], + ['root', 'hunter3', 'Špatné uživatelské jméno nebo heslo'], + ]; + } + + + /** + * @dataProvider getCredentials + */ + public function testCreateOnSuccess(string $username, string $password, ?string $message): void + { + $this->database->setFetchResult([ + 'userId' => 123, + 'username' => 'root', + 'password' => $this->passwordEncryption->encrypt($this->passwords->hash('hunter2')), + ]); + $this->formComponents->setValue($this->form, 'username', $username); + $this->formComponents->setValue($this->form, 'password', $password); + Arrays::invoke($this->form->onSuccess, $this->form); + if ($message === null) { + Assert::true($this->user->isLoggedIn()); + Assert::count(0, $this->form->getErrors()); + Assert::same(['Successful sign-in attempt (root, 127.31.33.7)'], $this->logger->getLogged()); + } else { + Assert::false($this->user->isLoggedIn()); + Assert::count(1, $this->form->getErrors()); + $formError = $this->form->getErrors()[0]; + assert(is_string($formError) || $formError instanceof Stringable); + Assert::same($message, (string)$formError); + Assert::same(['Failed sign-in attempt: The password is incorrect. (root, 127.31.33.7)'], $this->logger->getLogged()); + } + } + +} + +TestCaseRunner::run(SignInFormFactoryTest::class); diff --git a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt index 8cfa46af9..205917206 100644 --- a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt +++ b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt @@ -5,8 +5,8 @@ declare(strict_types = 1); namespace MichalSpacekCz\Form; use MichalSpacekCz\Test\Application\ApplicationPresenter; +use MichalSpacekCz\Test\Form\FormComponents; use MichalSpacekCz\Test\TestCaseRunner; -use Nette\Forms\Controls\TextInput; use Nette\Utils\Arrays; use Override; use Stringable; @@ -23,6 +23,7 @@ class SignInHoneypotFormFactoryTest extends TestCase public function __construct( + private readonly FormComponents $formComponents, SignInHoneypotFormFactory $signInHoneypotFormFactory, ApplicationPresenter $applicationPresenter, ) { @@ -57,22 +58,14 @@ class SignInHoneypotFormFactoryTest extends TestCase /** @dataProvider getCredentials */ public function testCreateOnSuccess(string $username, string $password, string $error): void { - $this->setValue('username', $username); - $this->setValue('password', $password); + $this->formComponents->setValue($this->form, 'username', $username); + $this->formComponents->setValue($this->form, 'password', $password); Arrays::invoke($this->form->onSuccess, $this->form); $formError = $this->form->getErrors()[0]; assert(is_string($formError) || $formError instanceof Stringable); Assert::same($error, (string)$formError); } - - private function setValue(string $component, string $value): void - { - $field = $this->form->getComponent($component); - assert($field instanceof TextInput); - $field->setDefaultValue($value); - } - } TestCaseRunner::run(SignInHoneypotFormFactoryTest::class); diff --git a/app/tests/Form/TalkSlidesFormFactoryTest.phpt b/app/tests/Form/TalkSlidesFormFactoryTest.phpt index 96cf70332..098f30c4d 100644 --- a/app/tests/Form/TalkSlidesFormFactoryTest.phpt +++ b/app/tests/Form/TalkSlidesFormFactoryTest.phpt @@ -30,7 +30,7 @@ class TalkSlidesFormFactoryTest extends TestCase } - public function testCreateOnsuccess(): void + public function testCreateOnSuccess(): void { $talkId = 123; $onSuccessMessage = $onSuccessType = $onSuccessTalkId = $result = null; diff --git a/app/tests/Form/TrainingFileFormFactoryTest.phpt b/app/tests/Form/TrainingFileFormFactoryTest.phpt new file mode 100644 index 000000000..e419b2d7f --- /dev/null +++ b/app/tests/Form/TrainingFileFormFactoryTest.phpt @@ -0,0 +1,53 @@ +form = $formFactory->create( + function (Html|string $message, string $type) { + $this->message = (string)$message; + $this->type = $type; + }, + new DateTimeImmutable(), + [], + ); + $presenter = $applicationPresenter->createUiPresenter('Admin:Trainings', 'foo', 'file'); + /** @noinspection PhpInternalEntityUsedInspection */ + $this->form->setParent($presenter); + } + + + public function testCreateOnSuccessError(): void + { + Arrays::invoke($this->form->onSuccess, $this->form); + Assert::same('Soubor nebyl vybrán nebo došlo k nějaké chybě při nahrávání', $this->message); + Assert::same('error', $this->type); + } + +} + +TestCaseRunner::run(TrainingFileFormFactoryTest::class); diff --git a/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt b/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt new file mode 100644 index 000000000..56cf40491 --- /dev/null +++ b/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt @@ -0,0 +1,50 @@ +form = $formFactory->create( + function (string $ssid) { + $this->ssid = $ssid; + }, + null, + ); + $presenter = $applicationPresenter->createUiPresenter('UpcKeys:Homepage', 'foo', 'default'); + /** @noinspection PhpInternalEntityUsedInspection */ + $this->form->setParent($presenter); + } + + + public function testCreateOnSuccessError(): void + { + $this->formComponents->setValue($this->form, 'ssid', ' abc123 '); + Arrays::invoke($this->form->onSuccess, $this->form); + Assert::same('ABC123', $this->ssid); + } + +} + +TestCaseRunner::run(UpcKeysSsidFormFactoryTest::class); From 19e8c6a9e409b77f873fa5c293912b6fbcc557f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sat, 30 Nov 2024 02:58:09 +0100 Subject: [PATCH 18/24] Disallow Form::getValues() instead of deprecating it in a child class --- app/disallowed-calls.neon | 14 ++++++-------- app/phpstan-vendor.neon | 7 +++++++ app/psalm-baseline.xml | 5 ----- app/src/Form/UiForm.php | 30 ------------------------------ 4 files changed, 13 insertions(+), 43 deletions(-) diff --git a/app/disallowed-calls.neon b/app/disallowed-calls.neon index 56407d53c..202493daf 100644 --- a/app/disallowed-calls.neon +++ b/app/disallowed-calls.neon @@ -56,6 +56,12 @@ parameters: message: 'use MichalSpacekCz\DateTime\DateTimeZoneFactory::get() instead, throws a more specific exception' allowInMethods: - 'MichalSpacekCz\DateTime\DateTimeZoneFactory::get()' + - + method: + - 'Nette\Forms\Container::getValues()' + - 'Nette\Forms\Container::getUntrustedValues()' + message: 'use methods from MichalSpacekCz\Form\UiForm instead' + disallowedConstants: - constant: 'LIBXML_NOENT' @@ -68,14 +74,6 @@ parameters: - src/Application/ServerEnv.php - tests/Application/ServerEnvTest.phpt disallowedClasses: - - - class: - - 'Nette\Application\UI\Form' - - 'Nette\Forms\Form' - message: 'use MichalSpacekCz\Form\UiForm for better type declarations' - allowIn: - - src/Form/UiForm.php - - src/Form/Controls/TrainingControlsFactory.php - class: - 'Spaze\PhpInfo\PhpInfo' diff --git a/app/phpstan-vendor.neon b/app/phpstan-vendor.neon index b91c92947..db23c532d 100644 --- a/app/phpstan-vendor.neon +++ b/app/phpstan-vendor.neon @@ -202,6 +202,13 @@ parameters: message: 'use MichalSpacekCz\DateTime\DateTimeZoneFactory::get() instead, throws a more specific exception' allowIn: - vendor/*.php + - + method: + - 'Nette\Forms\Container::getValues()' + - 'Nette\Forms\Container::getUntrustedValues()' + message: 'use methods from MichalSpacekCz\Form\UiForm instead' + allowIn: + - vendor/nette/*.php disallowedStaticCalls: - method: 'Tracy\Debugger::barDump()' diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 77a44a36b..e41232884 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -246,11 +246,6 @@ review]]> - - - ssid]]> - - diff --git a/app/src/Form/UiForm.php b/app/src/Form/UiForm.php index 7295dd9ae..795085f26 100644 --- a/app/src/Form/UiForm.php +++ b/app/src/Form/UiForm.php @@ -6,7 +6,6 @@ use MichalSpacekCz\ShouldNotHappenException; use Nette\Application\UI\Form; use Nette\Utils\ArrayHash; -use Override; class UiForm extends Form { @@ -30,33 +29,4 @@ public function getUntrustedFormValues(): ArrayHash return $values; } - - /** - * @return object|array - * @deprecated Use getFormValues() instead - */ - #[Override] - public function getValues(string|object|bool|null $returnType = null, ?array $controls = null): object|array - { - if (func_num_args() === 0) { - trigger_error('Use getFormValues() instead', E_USER_DEPRECATED); - } - return parent::getValues($returnType, $controls); - } - - - /** - * @param string|object|null $returnType - * @return object|array - * @deprecated Use getUntrustedFormValues() instead - * */ - #[Override] - public function getUntrustedValues($returnType = ArrayHash::class, ?array $controls = null): object|array - { - if (func_num_args() === 0) { - trigger_error('Use getUntrustedFormValues() instead', E_USER_DEPRECATED); - } - return parent::getUntrustedValues($returnType, $controls); - } - } From 00120038dc782f47425cae78e6a4fa8e945c8768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sat, 30 Nov 2024 04:38:45 +0100 Subject: [PATCH 19/24] Use form rule validator constants on the Form class, not on the object --- app/src/Form/ChangePasswordFormFactory.php | 5 +- app/src/Form/InterviewFormFactory.php | 19 +++---- app/src/Form/PostFormFactory.php | 25 +++++----- .../PasswordsStorageAlgorithmFormFactory.php | 49 ++++++++++--------- app/src/Form/TalkFormFactory.php | 29 +++++------ app/src/Form/TalkSlidesFormFactory.php | 9 ++-- .../TrainingApplicationAdminFormFactory.php | 5 +- .../Form/TrainingApplicationFormFactory.php | 3 +- app/src/Form/TrainingDateFormFactory.php | 27 +++++----- app/src/Form/TrainingInvoiceFormFactory.php | 3 +- .../Form/TrainingMailsOutboxFormFactory.php | 11 +++-- app/src/Form/TrainingReviewFormFactory.php | 17 ++++--- app/src/Form/UpcKeysSsidFormFactory.php | 3 +- app/src/Media/VideoThumbnails.php | 21 ++++---- 14 files changed, 120 insertions(+), 106 deletions(-) diff --git a/app/src/Form/ChangePasswordFormFactory.php b/app/src/Form/ChangePasswordFormFactory.php index 0a3ffd1fd..57fb35590 100644 --- a/app/src/Form/ChangePasswordFormFactory.php +++ b/app/src/Form/ChangePasswordFormFactory.php @@ -5,6 +5,7 @@ use MichalSpacekCz\User\Exceptions\IdentityException; use MichalSpacekCz\User\Manager; +use Nette\Forms\Form; use Nette\Security\User; readonly class ChangePasswordFormFactory @@ -36,11 +37,11 @@ public function create(callable $onSuccess): UiForm ->setHtmlAttribute('autocomplete', 'new-password') ->setHtmlAttribute('passwordrules', 'minlength: 42; required: lower; required: upper; required: digit; required: [ !#$%&*+,./:;=?@_~];') ->setRequired('Zadejte prosím nové heslo') - ->addRule($form::MinLength, 'Nové heslo musí mít alespoň %d znaků', 15); + ->addRule(Form::MinLength, 'Nové heslo musí mít alespoň %d znaků', 15); $form->addPassword('newPasswordVerify', 'Nové heslo pro kontrolu:') ->setHtmlAttribute('autocomplete', 'new-password') ->setRequired('Zadejte prosím nové heslo pro kontrolu') - ->addRule($form::Equal, 'Hesla se neshodují', $newPassword); + ->addRule(Form::Equal, 'Hesla se neshodují', $newPassword); $form->addSubmit('save', 'Uložit'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess): void { diff --git a/app/src/Form/InterviewFormFactory.php b/app/src/Form/InterviewFormFactory.php index 229f6e637..43ddba884 100644 --- a/app/src/Form/InterviewFormFactory.php +++ b/app/src/Form/InterviewFormFactory.php @@ -8,6 +8,7 @@ use MichalSpacekCz\Interviews\Interviews; use MichalSpacekCz\Media\VideoThumbnails; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; readonly class InterviewFormFactory { @@ -29,10 +30,10 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor $form = $this->factory->create(); $form->addText('action', 'Akce:') ->setRequired('Zadejte prosím akci') - ->addRule($form::MaxLength, 'Maximální délka akce je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka akce je %d znaků', 200); $form->addText('title', 'Název:') ->setRequired('Zadejte prosím název') - ->addRule($form::MaxLength, 'Maximální délka názvu je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka názvu je %d znaků', 200); $form->addTextArea('description', 'Popis:') ->setRequired(false); $this->trainingControlsFactory->addDate( @@ -43,26 +44,26 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor ); $form->addText('href', 'Odkaz na rozhovor:') ->setRequired('Zadejte prosím odkaz na rozhovor') - ->addRule($form::MaxLength, 'Maximální délka odkazu na rozhovor je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na rozhovor je %d znaků', 200); $form->addText('audioHref', 'Odkaz na audio:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na audio je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na audio je %d znaků', 200); $form->addText('audioEmbed', 'Embed odkaz na audio:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka embed odkazu na audio je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka embed odkazu na audio je %d znaků', 200); $form->addText('videoHref', 'Odkaz na video:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na video je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na video je %d znaků', 200); $videoThumbnailFormFields = $this->videoThumbnails->addFormFields($form, $interview?->getVideo()->getThumbnailFilename() !== null, $interview?->getVideo()->getThumbnailAlternativeFilename() !== null); $form->addText('videoEmbed', 'Embed odkaz na video:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka embed odkazu na video je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka embed odkazu na video je %d znaků', 200); $form->addText('sourceName', 'Název zdroje:') ->setRequired('Zadejte prosím název zdroje') - ->addRule($form::MaxLength, 'Maximální délka názvu zdroje je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka názvu zdroje je %d znaků', 200); $form->addText('sourceHref', 'Odkaz na zdroj:') ->setRequired('Zadejte prosím odkaz na zdroj') - ->addRule($form::MaxLength, 'Maximální délka odkazu na zdroj je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na zdroj je %d znaků', 200); $submit = $form->addSubmit('submit', 'Přidat'); if ($interview) { $this->setInterview($form, $interview, $submit); diff --git a/app/src/Form/PostFormFactory.php b/app/src/Form/PostFormFactory.php index 24601c062..cbb90533e 100644 --- a/app/src/Form/PostFormFactory.php +++ b/app/src/Form/PostFormFactory.php @@ -23,6 +23,7 @@ use Nette\Database\UniqueConstraintViolationException; use Nette\Forms\Controls\SubmitButton; use Nette\Forms\Controls\TextInput; +use Nette\Forms\Form; use Nette\Utils\Html; use Nette\Utils\Json; use Nette\Utils\JsonException; @@ -61,27 +62,27 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, DefaultT ->setPrompt('- vyberte -'); $form->addText('title', 'Titulek:') ->setRequired('Zadejte prosím titulek') - ->addRule($form::MinLength, 'Titulek musí mít alespoň %d znaky', 3); + ->addRule(Form::MinLength, 'Titulek musí mít alespoň %d znaky', 3); $form->addText('slug', 'Slug:') - ->addRule($form::MinLength, 'Slug musí mít alespoň %d znaky', 3); + ->addRule(Form::MinLength, 'Slug musí mít alespoň %d znaky', 3); $this->addPublishedDate($form->addText('published', 'Vydáno:')) ->setDefaultValue(date('Y-m-d') . ' HH:MM'); $previewKeyInput = $form->addText('previewKey', 'Klíč pro náhled:') ->setRequired(false) ->setDefaultValue(Random::generate(9, '0-9a-zA-Z')) - ->addRule($form::MinLength, 'Klíč pro náhled musí mít alespoň %d znaky', 3); + ->addRule(Form::MinLength, 'Klíč pro náhled musí mít alespoň %d znaky', 3); $form->addTextArea('lead', 'Perex:') - ->addCondition($form::Filled) - ->addRule($form::MinLength, 'Perex musí mít alespoň %d znaky', 3); + ->addCondition(Form::Filled) + ->addRule(Form::MinLength, 'Perex musí mít alespoň %d znaky', 3); $form->addTextArea('text', 'Text:') ->setRequired('Zadejte prosím text') - ->addRule($form::MinLength, 'Text musí mít alespoň %d znaky', 3); + ->addRule(Form::MinLength, 'Text musí mít alespoň %d znaky', 3); $form->addTextArea('originally', 'Původně vydáno:') - ->addCondition($form::Filled) - ->addRule($form::MinLength, 'Původně vydáno musí mít alespoň %d znaky', 3); + ->addCondition(Form::Filled) + ->addRule(Form::MinLength, 'Původně vydáno musí mít alespoň %d znaky', 3); $form->addText('ogImage', 'Odkaz na obrázek:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na obrázek je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na obrázek je %d znaků', 200); $cards = ['' => 'Žádná karta']; foreach ($this->twitterCards->getAll() as $card) { @@ -96,10 +97,10 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, DefaultT $editSummaryInput = $form->addText('editSummary', 'Shrnutí editace:'); $editSummaryInput->setRequired(false) ->setDisabled(true) - ->addCondition($form::Filled) - ->addRule($form::MinLength, 'Shrnutí editace musí mít alespoň %d znaky', 3) + ->addCondition(Form::Filled) + ->addRule(Form::MinLength, 'Shrnutí editace musí mít alespoň %d znaky', 3) ->endCondition() - ->addRule($form::MaxLength, 'Maximální délka shrnutí editace je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka shrnutí editace je %d znaků', 200); $label = Html::el()->addText(Html::el('span', ['title' => 'Content Security Policy'])->setText('CSP'))->addText(' snippety:'); $items = []; diff --git a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php index 2537909b7..7bd345c2a 100644 --- a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php +++ b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php @@ -12,6 +12,7 @@ use MichalSpacekCz\Pulse\Passwords\Passwords; use MichalSpacekCz\Pulse\Passwords\Storage\StorageSpecificSite; use MichalSpacekCz\Pulse\Sites; +use Nette\Forms\Form; use Nette\Utils\ArrayHash; readonly class PasswordsStorageAlgorithmFormFactory @@ -49,13 +50,13 @@ public function create(callable $onSuccess, int $newDisclosures): UiForm $newCompanyContainer->addText('dba', 'Trade name:') ->setHtmlAttribute('title', '"doing business as"'); $inputAlias = $newCompanyContainer->addText('alias', 'Alias:'); - $inputAlias->addConditionOn($inputName, $form::Filled) + $inputAlias->addConditionOn($inputName, Form::Filled) ->setRequired('Enter new company alias'); - $selectCompany->addConditionOn($inputName, $form::Blank) + $selectCompany->addConditionOn($inputName, Form::Blank) ->setRequired('Choose company or add a new one'); - $inputName->addConditionOn($selectCompany, $form::Filled) - ->addRule($form::Blank, "Company already selected, can't add a new one"); + $inputName->addConditionOn($selectCompany, Form::Filled) + ->addRule(Form::Blank, "Company already selected, can't add a new one"); // Site $siteContainer = $form->addContainer('site'); @@ -71,22 +72,22 @@ public function create(callable $onSuccess, int $newDisclosures): UiForm $inputAlias = $newSiteContainer->addText('alias', 'Alias:'); $newSiteContainer->addText('sharedWith', 'Storage shared with:'); - $selectSite->addConditionOn($inputUrl, $form::Blank) + $selectSite->addConditionOn($inputUrl, Form::Blank) ->setRequired('Choose site or add a new one'); - $inputUrl->addCondition($form::Filled) // intentionally addCondition(), there's a matching endCondition() below - ->addRule($form::URL, 'Incorrect site URL') + $inputUrl->addCondition(Form::Filled) // intentionally addCondition(), there's a matching endCondition() below + ->addRule(Form::URL, 'Incorrect site URL') ->endCondition() - ->addConditionOn($selectSite, $form::Filled) - ->addRule($form::Blank, $message = "Site already selected, can't add a new one") + ->addConditionOn($selectSite, Form::Filled) + ->addRule(Form::Blank, $message = "Site already selected, can't add a new one") ->endCondition() ->addCondition(function () use ($inputName, $selectSite): bool { return (!empty($inputName->getValue()) && $selectSite->getValue() !== Sites::ALL); }) ->setRequired('New site required when adding a new company'); - $inputAlias->addConditionOn($selectSite, $form::Filled) - ->addRule($form::Blank, $message) + $inputAlias->addConditionOn($selectSite, Form::Filled) + ->addRule(Form::Blank, $message) ->endCondition() - ->addConditionOn($inputUrl, $form::Filled) + ->addConditionOn($inputUrl, Form::Filled) ->setRequired('Enter new site alias'); // Algo @@ -112,14 +113,14 @@ public function create(callable $onSuccess, int $newDisclosures): UiForm $newAlgoContainer->addCheckbox('salted', 'Salted:'); $newAlgoContainer->addCheckbox('stretched', 'Stretched:'); - $selectAlgo->addConditionOn($inputAlgo, $form::Blank) + $selectAlgo->addConditionOn($inputAlgo, Form::Blank) ->setRequired('Choose algorithm or add a new one'); - $inputAlgo->addConditionOn($selectAlgo, $form::Filled) - ->addRule($form::Blank, $message = "Algorithm already selected, can't add a new one"); - $inputAlias->addConditionOn($selectAlgo, $form::Filled) - ->addRule($form::Blank, $message) + $inputAlgo->addConditionOn($selectAlgo, Form::Filled) + ->addRule(Form::Blank, $message = "Algorithm already selected, can't add a new one"); + $inputAlias->addConditionOn($selectAlgo, Form::Filled) + ->addRule(Form::Blank, $message) ->endCondition() - ->addConditionOn($inputAlgo, $form::Filled) + ->addConditionOn($inputAlgo, Form::Filled) ->setRequired('Enter new algorithm alias'); // Disclosures @@ -147,17 +148,17 @@ public function create(callable $onSuccess, int $newDisclosures): UiForm if ($i == 0) { $selectDisclosure->setRequired('Enter at least one disclosure type'); } else { - $selectDisclosure->addConditionOn($inputUrl, $form::Filled) + $selectDisclosure->addConditionOn($inputUrl, Form::Filled) ->setRequired('Enter disclosure type'); } - $inputUrl->addCondition($form::Filled) // intentionally addCondition(), there's a matching endCondition() below - ->addRule($form::URL, 'Incorrect disclosure URL') + $inputUrl->addCondition(Form::Filled) // intentionally addCondition(), there's a matching endCondition() below + ->addRule(Form::URL, 'Incorrect disclosure URL') ->endCondition() - ->addConditionOn($selectDisclosure, $form::Filled) + ->addConditionOn($selectDisclosure, Form::Filled) ->setRequired('Enter disclosure URL'); - $inputArchive->addConditionOn($inputUrl, $form::Filled) + $inputArchive->addConditionOn($inputUrl, Form::Filled) ->setRequired('Enter disclosure archive'); - $inputPublished->addConditionOn($selectDisclosure, $form::Filled) + $inputPublished->addConditionOn($selectDisclosure, Form::Filled) ->setRequired('Enter disclosure publish date'); } diff --git a/app/src/Form/TalkFormFactory.php b/app/src/Form/TalkFormFactory.php index f90bb2a50..a024d855a 100644 --- a/app/src/Form/TalkFormFactory.php +++ b/app/src/Form/TalkFormFactory.php @@ -10,6 +10,7 @@ use MichalSpacekCz\Talks\Talk; use MichalSpacekCz\Talks\Talks; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; use Nette\Utils\Html; use Nette\Utils\Strings; @@ -42,13 +43,13 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm ->setPrompt('- vyberte -'); $form->addText('action', 'Akce:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka akce je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka akce je %d znaků', 200); $form->addText('title', 'Název:') ->setRequired('Zadejte prosím název') - ->addRule($form::MaxLength, 'Maximální délka názvu je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka názvu je %d znaků', 200); $form->addTextArea('description', 'Popis:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka popisu je %d znaků', 65535); + ->addRule(Form::MaxLength, 'Maximální délka popisu je %d znaků', 65535); $this->trainingControlsFactory->addDate( $form->addText('date', 'Datum:'), true, @@ -57,7 +58,7 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm ); $form->addText('href', 'Odkaz na přednášku:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na přednášku je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na přednášku je %d znaků', 200); $form->addText('duration', 'Délka:') ->setHtmlType('number'); $form->addSelect('slidesTalk', 'Použít slajdy z:', $allTalks) @@ -66,35 +67,35 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm ->setPrompt('Vyberte prosím přednášku, ze které se použijí soubory pro slajdy'); $form->addText('slidesHref', 'Odkaz na slajdy:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na slajdy je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na slajdy je %d znaků', 200); $form->addText('slidesEmbed', 'Embed odkaz na slajdy:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka embed odkazu na slajdy je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka embed odkazu na slajdy je %d znaků', 200); $form->addTextArea('slidesNote', 'Poznámka ke slajdům:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka poznámek je %d znaků', 65535); + ->addRule(Form::MaxLength, 'Maximální délka poznámek je %d znaků', 65535); $form->addText('videoHref', 'Odkaz na video:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na video je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na video je %d znaků', 200); $videoThumbnailFormFields = $this->videoThumbnails->addFormFields($form, $talk?->getVideo()->getThumbnailFilename() !== null, $talk?->getVideo()->getThumbnailAlternativeContentType() !== null); $form->addText('videoEmbed', 'Embed odkaz na video:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka embed odkazu na video je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka embed odkazu na video je %d znaků', 200); $form->addText('event', 'Událost:') ->setRequired('Zadejte prosím událost') - ->addRule($form::MaxLength, 'Maximální délka události je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka události je %d znaků', 200); $form->addText('eventHref', 'Odkaz na událost:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na událost je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na událost je %d znaků', 200); $form->addText('ogImage', 'Odkaz na obrázek:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu na obrázek je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu na obrázek je %d znaků', 200); $form->addTextArea('transcript', 'Přepis:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka přepisu je %d znaků', 65535); + ->addRule(Form::MaxLength, 'Maximální délka přepisu je %d znaků', 65535); $form->addTextArea('favorite', 'Popis pro oblíbené:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka popisu pro oblíbené je %d znaků', 65535); + ->addRule(Form::MaxLength, 'Maximální délka popisu pro oblíbené je %d znaků', 65535); $form->addSelect('supersededBy', 'Nahrazeno přednáškou:', $allTalks) ->setPrompt('Vyberte prosím přednášku, kterou se tato nahradí'); $form->addCheckbox('publishSlides', 'Publikovat slajdy:'); diff --git a/app/src/Form/TalkSlidesFormFactory.php b/app/src/Form/TalkSlidesFormFactory.php index 785839fbb..ad3c4c0a8 100644 --- a/app/src/Form/TalkSlidesFormFactory.php +++ b/app/src/Form/TalkSlidesFormFactory.php @@ -10,6 +10,7 @@ use MichalSpacekCz\Talks\Slides\TalkSlides; use Nette\Application\Request; use Nette\Forms\Container; +use Nette\Forms\Form; use Nette\Utils\Html; readonly class TalkSlidesFormFactory @@ -93,7 +94,7 @@ private function addSlideFields(UiForm $form, Container $container, ?int $filena $disableSlideUploads = (bool)$filenamesTalkId; $container->addText('alias', 'Alias:') ->setRequired('Zadejte prosím alias') - ->addRule($form::Pattern, 'Alias musí být ve formátu [_.,a-z0-9-]+', '[_.,a-z0-9-]+'); + ->addRule(Form::Pattern, 'Alias musí být ve formátu [_.,a-z0-9-]+', '[_.,a-z0-9-]+'); $container->addInteger('number', 'Slajd:') ->setHtmlType('number') ->setDefaultValue(1) @@ -103,17 +104,17 @@ private function addSlideFields(UiForm $form, Container $container, ?int $filena ->setRequired('Zadejte prosím titulek'); $upload = $container->addUpload('replace', 'Nahradit:') ->setDisabled($disableSlideUploads) - ->addRule($form::MimeType, "Soubor musí být obrázek typu {$supportedImages}", $this->supportedImageFileFormats->getMainContentTypes()) + ->addRule(Form::MimeType, "Soubor musí být obrázek typu {$supportedImages}", $this->supportedImageFileFormats->getMainContentTypes()) ->setHtmlAttribute('title', "Nahradit soubor ({$supportedImages})") ->setHtmlAttribute('accept', implode(',', $this->supportedImageFileFormats->getMainContentTypes())); $container->addText('filename', 'Soubor:') ->setDisabled($disableSlideUploads) ->setHtmlAttribute('class', 'slide-filename') - ->addConditionOn($upload, $form::Blank) + ->addConditionOn($upload, Form::Blank) ->setRequired('Zadejte prosím soubor'); $container->addUpload('replaceAlternative', 'Nahradit:') ->setDisabled($disableSlideUploads) - ->addRule($form::MimeType, "Alternativní soubor musí být obrázek typu {$supportedAlternativeImages}", $this->supportedImageFileFormats->getAlternativeContentTypes()) + ->addRule(Form::MimeType, "Alternativní soubor musí být obrázek typu {$supportedAlternativeImages}", $this->supportedImageFileFormats->getAlternativeContentTypes()) ->setHtmlAttribute('title', "Nahradit alternativní soubor ({$supportedAlternativeImages})") ->setHtmlAttribute('accept', implode(',', $this->supportedImageFileFormats->getAlternativeContentTypes())); $container->addText('filenameAlternative', 'Soubor:') diff --git a/app/src/Form/TrainingApplicationAdminFormFactory.php b/app/src/Form/TrainingApplicationAdminFormFactory.php index 902395bc0..15976a117 100644 --- a/app/src/Form/TrainingApplicationAdminFormFactory.php +++ b/app/src/Form/TrainingApplicationAdminFormFactory.php @@ -15,6 +15,7 @@ use Nette\Forms\Controls\BaseControl; use Nette\Forms\Controls\Checkbox; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; readonly class TrainingApplicationAdminFormFactory { @@ -125,14 +126,14 @@ private function addPaymentInfo(UiForm $form): void $form->addText('price', 'Cena bez DPH:') ->setHtmlType('number') ->setHtmlAttribute('step', 'any') - ->addRule($form::Float) + ->addRule(Form::Float) ->setHtmlAttribute('title', 'Po případné slevě'); $form->addText('vatRate', 'DPH:') ->setHtmlType('number'); $form->addText('priceVat', 'Cena s DPH:') ->setHtmlType('number') ->setHtmlAttribute('step', 'any') - ->addRule($form::Float) + ->addRule(Form::Float) ->setHtmlAttribute('title', 'Po případné slevě'); $form->addText('discount', 'Sleva:') ->setHtmlType('number'); diff --git a/app/src/Form/TrainingApplicationFormFactory.php b/app/src/Form/TrainingApplicationFormFactory.php index d56bde80a..053c2f601 100644 --- a/app/src/Form/TrainingApplicationFormFactory.php +++ b/app/src/Form/TrainingApplicationFormFactory.php @@ -13,6 +13,7 @@ use MichalSpacekCz\Training\Exceptions\TrainingDateNotRemoteNoVenueException; use Nette\Forms\Controls\SelectBox; use Nette\Forms\Controls\TextInput; +use Nette\Forms\Form; use Nette\Utils\Html; readonly class TrainingApplicationFormFactory @@ -63,7 +64,7 @@ public function create( $form->addSelect('trainingId', $this->translator->translate('label.trainingdate'), $inputDates) ->setRequired('Vyberte prosím termín a místo školení') ->setPrompt('- vyberte termín a místo -') - ->addRule($form::Integer); + ->addRule(Form::Integer); } $this->trainingControlsFactory->addAttendee($form); diff --git a/app/src/Form/TrainingDateFormFactory.php b/app/src/Form/TrainingDateFormFactory.php index fefda620a..68a17636c 100644 --- a/app/src/Form/TrainingDateFormFactory.php +++ b/app/src/Form/TrainingDateFormFactory.php @@ -11,6 +11,7 @@ use MichalSpacekCz\Training\Trainings\Trainings; use MichalSpacekCz\Training\Venues\TrainingVenues; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; readonly class TrainingDateFormFactory { @@ -71,12 +72,12 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin ->setHtmlAttribute('placeholder', 'YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM') ->setHtmlAttribute('title', 'Formát YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM') ->setRequired('Zadejte prosím začátek') - ->addRule($form::Pattern, 'Začátek musí být ve formátu YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM', '(\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2})|(\d{1,2}\.\d{1,2}\.\d{4} \d{1,2}:\d{2})'); + ->addRule(Form::Pattern, 'Začátek musí být ve formátu YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM', '(\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2})|(\d{1,2}\.\d{1,2}\.\d{4} \d{1,2}:\d{2})'); $end = $form->addText('end', 'Konec:') ->setHtmlAttribute('placeholder', 'YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM') ->setHtmlAttribute('title', 'Formát YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM') ->setRequired('Zadejte prosím konec') - ->addRule($form::Pattern, 'Konec musí být ve formátu YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM', '(\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2})|(\d{1,2}\.\d{1,2}\.\d{4} \d{1,2}:\d{2})'); + ->addRule(Form::Pattern, 'Konec musí být ve formátu YYYY-MM-DD HH:MM nebo DD.MM.YYYY HH:MM', '(\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2})|(\d{1,2}\.\d{1,2}\.\d{4} \d{1,2}:\d{2})'); $form->onValidate[] = function () use ($start, $end): void { $this->trainingDatesFormValidator->validateFormStartEnd($start, $end); }; @@ -92,15 +93,15 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin $checkboxPublic = $form->addCheckbox('public', 'Veřejné:'); $checkboxRemote = $form->addCheckbox('remote', 'Online:'); - $checkboxRemote->addConditionOn($selectVenue, $form::Filled) - ->addConditionOn($checkboxPublic, $form::Filled) - ->addRule($form::Blank, 'Je vybráno místo, veřejné školení nemůže být online'); - $selectVenue->addConditionOn($checkboxRemote, $form::Blank) + $checkboxRemote->addConditionOn($selectVenue, Form::Filled) + ->addConditionOn($checkboxPublic, Form::Filled) + ->addRule(Form::Blank, 'Je vybráno místo, veřejné školení nemůže být online'); + $selectVenue->addConditionOn($checkboxRemote, Form::Blank) ->setRequired('Vyberte prosím místo nebo školení označte jako online'); $form->addText('remoteUrl', 'Online URL:') - ->addRule($form::URL, 'Online URL musí být validní URL') - ->addRule($form::MaxLength, 'Maximální délka URL je %d znaků', 200); + ->addRule(Form::URL, 'Online URL musí být validní URL') + ->addRule(Form::MaxLength, 'Maximální délka URL je %d znaků', 200); $format = "Bez HTML značek,\nodřádkování bude v pozvánce zachováno"; $form->addTextArea('remoteNotes', 'Online poznámky:') @@ -108,7 +109,7 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin ->setHtmlAttribute('title', $format); $form->addSelect('cooperation', 'Spolupráce:', [0 => 'žádná'] + $this->trainings->getCooperations()) - ->addRule($form::Integer); + ->addRule(Form::Integer); $this->trainingControlsFactory->addNote($form); $price = $form->addText('price', 'Cena bez DPH:') @@ -127,11 +128,11 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin ->setHtmlAttribute('title', 'Ponechte prázdné, aby se použila běžná sleva'); $form->addText('videoHref', 'Odkaz na záznam:') - ->addRule($form::URL, 'Odkaz na záznam musí být validní URL') - ->addRule($form::MaxLength, 'Maximální délka URL je %d znaků', 200); + ->addRule(Form::URL, 'Odkaz na záznam musí být validní URL') + ->addRule(Form::MaxLength, 'Maximální délka URL je %d znaků', 200); $form->addText('feedbackHref', 'Odkaz na feedback formulář:') - ->addRule($form::URL, 'Odkaz na feedback formulář musí být validní URL') - ->addRule($form::MaxLength, 'Maximální délka URL je %d znaků', 200); + ->addRule(Form::URL, 'Odkaz na feedback formulář musí být validní URL') + ->addRule(Form::MaxLength, 'Maximální délka URL je %d znaků', 200); $submit = $form->addSubmit('submit', 'Přidat'); if ($date) { diff --git a/app/src/Form/TrainingInvoiceFormFactory.php b/app/src/Form/TrainingInvoiceFormFactory.php index 8915deab8..8f7843c8e 100644 --- a/app/src/Form/TrainingInvoiceFormFactory.php +++ b/app/src/Form/TrainingInvoiceFormFactory.php @@ -5,6 +5,7 @@ use MichalSpacekCz\Form\Controls\TrainingControlsFactory; use MichalSpacekCz\Training\Applications\TrainingApplications; +use Nette\Forms\Form; readonly class TrainingInvoiceFormFactory { @@ -27,7 +28,7 @@ public function create(callable $onSuccess, callable $onError, array $unpaidInvo $form = $this->factory->create(); $form->addText('invoice', 'Faktura:') ->setRequired('Zadejte prosím číslo faktury') - ->addRule($form::IsIn, 'Zadejte číslo některé z nezaplacených faktur', $unpaidInvoiceIds); + ->addRule(Form::IsIn, 'Zadejte číslo některé z nezaplacených faktur', $unpaidInvoiceIds); $this->trainingControlsFactory->addPaidDate($form->addText('paid', 'Zaplaceno:'), true); $form->addSubmit('submit', 'Zaplaceno'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess, $onError): void { diff --git a/app/src/Form/TrainingMailsOutboxFormFactory.php b/app/src/Form/TrainingMailsOutboxFormFactory.php index 29b5c005b..dd97a25db 100644 --- a/app/src/Form/TrainingMailsOutboxFormFactory.php +++ b/app/src/Form/TrainingMailsOutboxFormFactory.php @@ -12,6 +12,7 @@ use MichalSpacekCz\Training\Mails\TrainingMails; use Nette\Application\Application as NetteApplication; use Nette\Application\UI\Presenter; +use Nette\Forms\Form; use stdClass; readonly class TrainingMailsOutboxFormFactory @@ -129,14 +130,14 @@ public function create(callable $onSuccess, array $applications): UiForm ->setHtmlAttribute('placeholder', 'Faktura č.') ->setHtmlAttribute('title', 'Faktura č.') ->setDefaultValue($application->getInvoiceId()) - ->addConditionOn($send, $form::Filled) - ->addRule($form::Filled, 'Chybí číslo faktury'); + ->addConditionOn($send, Form::Filled) + ->addRule(Form::Filled, 'Chybí číslo faktury'); $applicationIdsContainer->addUpload('invoice') ->setHtmlAttribute('title', 'Faktura v PDF') ->setHtmlAttribute('accept', 'application/pdf') - ->addConditionOn($send, $form::Filled) - ->addRule($form::Filled, 'Chybí faktura') - ->addRule($form::MimeType, 'Faktura není v PDF', 'application/pdf'); + ->addConditionOn($send, Form::Filled) + ->addRule(Form::Filled, 'Chybí faktura') + ->addRule(Form::MimeType, 'Faktura není v PDF', 'application/pdf'); $applicationIdsContainer->addEmail('cc', 'Cc:')->setRequired(false); break; } diff --git a/app/src/Form/TrainingReviewFormFactory.php b/app/src/Form/TrainingReviewFormFactory.php index e3d933bdf..22d1c6ce6 100644 --- a/app/src/Form/TrainingReviewFormFactory.php +++ b/app/src/Form/TrainingReviewFormFactory.php @@ -7,6 +7,7 @@ use MichalSpacekCz\Training\Reviews\TrainingReview; use MichalSpacekCz\Training\Reviews\TrainingReviews; use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; use Nette\Utils\Html; readonly class TrainingReviewFormFactory @@ -34,28 +35,28 @@ public function create(callable $onSuccess, int $dateId, ?TrainingReview $review } $form->addText('name', 'Jméno:') ->setRequired('Zadejte prosím jméno') - ->addRule($form::MinLength, 'Minimální délka jména je %d znaky', 3) - ->addRule($form::MaxLength, 'Maximální délka jména je %d znaků', 200); + ->addRule(Form::MinLength, 'Minimální délka jména je %d znaky', 3) + ->addRule(Form::MaxLength, 'Maximální délka jména je %d znaků', 200); $form->addText('company', 'Firma:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka firmy je %d znaků', 200); // No min length to allow _removal_ of company name from a review by using an empty string + ->addRule(Form::MaxLength, 'Maximální délka firmy je %d znaků', 200); // No min length to allow _removal_ of company name from a review by using an empty string $form->addText('jobTitle', 'Pozice:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka pozice je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka pozice je %d znaků', 200); $form->addTextArea('review', 'Ohlas:') ->setRequired('Zadejte prosím ohlas') - ->addRule($form::MinLength, 'Minimální délka ohlasu je %d znaky', 3) - ->addRule($form::MaxLength, 'Maximální délka ohlasu je %d znaků', 2000); + ->addRule(Form::MinLength, 'Minimální délka ohlasu je %d znaky', 3) + ->addRule(Form::MaxLength, 'Maximální délka ohlasu je %d znaků', 2000); $form->addText('href', 'Odkaz:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka odkazu je %d znaků', 200); + ->addRule(Form::MaxLength, 'Maximální délka odkazu je %d znaků', 200); $form->addCheckbox('hidden', 'Skrýt:'); $form->addText('ranking', 'Pořadí:') ->setRequired(false) ->setHtmlType('number'); $form->addText('note', 'Poznámka:') ->setRequired(false) - ->addRule($form::MaxLength, 'Maximální délka poznámky je %d znaků', 2000); + ->addRule(Form::MaxLength, 'Maximální délka poznámky je %d znaků', 2000); $submit = $form->addSubmit('submit', 'Přidat'); if ($review) { $this->setReview($form, $review, $submit); diff --git a/app/src/Form/UpcKeysSsidFormFactory.php b/app/src/Form/UpcKeysSsidFormFactory.php index 6933b9f0e..546540aec 100644 --- a/app/src/Form/UpcKeysSsidFormFactory.php +++ b/app/src/Form/UpcKeysSsidFormFactory.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Form; use MichalSpacekCz\UpcKeys\UpcKeys; +use Nette\Forms\Form; readonly class UpcKeysSsidFormFactory { @@ -26,7 +27,7 @@ public function create(callable $onSuccess, ?string $ssid): UiForm ->setHtmlAttribute('title', '"UPC" and 7 digits') ->setDefaultValue($ssid) ->setRequired('Please enter an SSID') - ->addRule($form::Pattern, 'Wi-Fi network name has to be "UPC" and 7 digits (UPC1234567)', '\s*' . $this->upcKeys->getValidSsidPattern() . '\s*'); + ->addRule(Form::Pattern, 'Wi-Fi network name has to be "UPC" and 7 digits (UPC1234567)', '\s*' . $this->upcKeys->getValidSsidPattern() . '\s*'); $form->addSubmit('submit', 'Get keys') ->setHtmlId('submit') ->setHtmlAttribute('data-alt', 'Wait…'); diff --git a/app/src/Media/VideoThumbnails.php b/app/src/Media/VideoThumbnails.php index 233606fa0..ec2b2b7fb 100644 --- a/app/src/Media/VideoThumbnails.php +++ b/app/src/Media/VideoThumbnails.php @@ -9,6 +9,7 @@ use MichalSpacekCz\Media\Exceptions\MissingContentTypeException; use MichalSpacekCz\Media\Resources\MediaResources; use Nette\Forms\Controls\UploadControl; +use Nette\Forms\Form; use Nette\Http\FileUpload; use Nette\Utils\Callback; use Nette\Utils\ImageException; @@ -45,29 +46,29 @@ public function addFormFields(UiForm $form, bool $hasMainVideoThumbnail, bool $h $supportedImages = '*.' . implode(', *.', $this->supportedImageFileFormats->getMainExtensions()); $supportedAlternativeImages = '*.' . implode(', *.', $this->supportedImageFileFormats->getAlternativeExtensions()); $videoThumbnail = $form->addUpload('videoThumbnail', 'Video náhled:') - ->addRule($form::MimeType, "%label musí být obrázek typu {$supportedImages}", $this->supportedImageFileFormats->getMainContentTypes()) + ->addRule(Form::MimeType, "%label musí být obrázek typu {$supportedImages}", $this->supportedImageFileFormats->getMainContentTypes()) ->setHtmlAttribute('title', "Vyberte soubor ({$supportedImages})") ->setHtmlAttribute('accept', implode(',', $this->supportedImageFileFormats->getMainContentTypes())); $videoThumbnailAlternative = $form->addUpload('videoThumbnailAlternative', 'Alternativní video náhled:') - ->addRule($form::MimeType, "%label musí být obrázek typu {$supportedAlternativeImages}", $this->supportedImageFileFormats->getAlternativeContentTypes()) + ->addRule(Form::MimeType, "%label musí být obrázek typu {$supportedAlternativeImages}", $this->supportedImageFileFormats->getAlternativeContentTypes()) ->setHtmlAttribute('title', "Vyberte alternativní soubor ({$supportedAlternativeImages})") ->setHtmlAttribute('accept', implode(',', $this->supportedImageFileFormats->getAlternativeContentTypes())); if ($hasMainVideoThumbnail) { $form->addCheckbox('removeVideoThumbnail', 'Odstranit') - ->addCondition($form::Filled, true) + ->addCondition(Form::Filled, true) ->toggle('#videoThumbnailFormField', false) - ->addConditionOn($videoThumbnail, $form::Filled, true) - ->addRule($form::Blank, 'Nelze zároveň nahrávat a mazat video náhled'); - $videoThumbnail->addCondition($form::Filled, true) + ->addConditionOn($videoThumbnail, Form::Filled, true) + ->addRule(Form::Blank, 'Nelze zároveň nahrávat a mazat video náhled'); + $videoThumbnail->addCondition(Form::Filled, true) ->toggle('#currentVideoThumbnail', false); } if ($hasAlternativeVideoThumbnail) { $form->addCheckbox('removeVideoThumbnailAlternative', 'Odstranit') - ->addCondition($form::Filled, true) + ->addCondition(Form::Filled, true) ->toggle('#videoThumbnailAlternativeFormField', false) - ->addConditionOn($videoThumbnailAlternative, $form::Filled, true) - ->addRule($form::Blank, 'Nelze zároveň nahrávat a mazat alternativní video náhled'); - $videoThumbnailAlternative->addCondition($form::Filled, true) + ->addConditionOn($videoThumbnailAlternative, Form::Filled, true) + ->addRule(Form::Blank, 'Nelze zároveň nahrávat a mazat alternativní video náhled'); + $videoThumbnailAlternative->addCondition(Form::Filled, true) ->toggle('#currentVideoThumbnailAlternative', false); } return new VideoThumbnailFileUploads($videoThumbnail, $videoThumbnailAlternative, $hasMainVideoThumbnail, $hasAlternativeVideoThumbnail); From 47445b8bd69ba44e72ca162d672c87c6a76a7ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sat, 30 Nov 2024 16:11:09 +0100 Subject: [PATCH 20/24] Anchor form method --- app/src/Test/Application/ApplicationPresenter.php | 9 +++++++++ app/tests/Form/SignInFormFactoryTest.phpt | 4 +--- app/tests/Form/SignInHoneypotFormFactoryTest.phpt | 4 +--- app/tests/Form/TalkSlidesFormFactoryTest.phpt | 8 +------- app/tests/Form/TrainingFileFormFactoryTest.phpt | 4 +--- app/tests/Form/UpcKeysSsidFormFactoryTest.phpt | 4 +--- app/tests/Pulse/Passwords/PasswordsTest.phpt | 8 +------- 7 files changed, 15 insertions(+), 26 deletions(-) diff --git a/app/src/Test/Application/ApplicationPresenter.php b/app/src/Test/Application/ApplicationPresenter.php index b3432cc40..51b821f3a 100644 --- a/app/src/Test/Application/ApplicationPresenter.php +++ b/app/src/Test/Application/ApplicationPresenter.php @@ -4,6 +4,7 @@ namespace MichalSpacekCz\Test\Application; use Closure; +use MichalSpacekCz\Form\UiForm; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\PrivateProperty; use Nette\Application\AbortException; @@ -78,4 +79,12 @@ public function createUiPresenter(string $existing, string $name, string $action return $presenter; } + + public function anchorForm(UiForm $form): void + { + $presenter = $this->createUiPresenter('Www:Homepage', 'foo', 'default'); + /** @noinspection PhpInternalEntityUsedInspection */ + $form->setParent($presenter); + } + } diff --git a/app/tests/Form/SignInFormFactoryTest.phpt b/app/tests/Form/SignInFormFactoryTest.phpt index 73cdf34dc..9d4ac1b17 100644 --- a/app/tests/Form/SignInFormFactoryTest.phpt +++ b/app/tests/Form/SignInFormFactoryTest.phpt @@ -44,9 +44,7 @@ class SignInFormFactoryTest extends TestCase $httpRequest->setRemoteAddress('127.31.33.7'); $this->form = $formFactory->create(function () { }); - $presenter = $applicationPresenter->createUiPresenter('Admin:Sign', 'foo', 'in'); - /** @noinspection PhpInternalEntityUsedInspection */ - $this->form->setParent($presenter); + $applicationPresenter->anchorForm($this->form); $service = $container->getService('passwordEncryption'); assert($service instanceof SymmetricKeyEncryption); diff --git a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt index 205917206..447ed0916 100644 --- a/app/tests/Form/SignInHoneypotFormFactoryTest.phpt +++ b/app/tests/Form/SignInHoneypotFormFactoryTest.phpt @@ -28,9 +28,7 @@ class SignInHoneypotFormFactoryTest extends TestCase ApplicationPresenter $applicationPresenter, ) { $this->form = $signInHoneypotFormFactory->create(); - $presenter = $applicationPresenter->createUiPresenter('Admin:Honeypot', 'foo', 'signIn'); - /** @noinspection PhpInternalEntityUsedInspection */ - $this->form->setParent($presenter); + $applicationPresenter->anchorForm($this->form); } diff --git a/app/tests/Form/TalkSlidesFormFactoryTest.phpt b/app/tests/Form/TalkSlidesFormFactoryTest.phpt index 098f30c4d..f00c5f18d 100644 --- a/app/tests/Form/TalkSlidesFormFactoryTest.phpt +++ b/app/tests/Form/TalkSlidesFormFactoryTest.phpt @@ -7,9 +7,7 @@ namespace MichalSpacekCz\Form; use MichalSpacekCz\Talks\Slides\TalkSlide; use MichalSpacekCz\Talks\Slides\TalkSlideCollection; use MichalSpacekCz\Test\Application\ApplicationPresenter; -use MichalSpacekCz\Test\PrivateProperty; use MichalSpacekCz\Test\TestCaseRunner; -use Nette\Application\Application; use Nette\Application\Request; use Nette\Utils\Arrays; use Nette\Utils\Html; @@ -24,7 +22,6 @@ class TalkSlidesFormFactoryTest extends TestCase public function __construct( private readonly TalkSlidesFormFactory $talkSlidesFormFactory, - private readonly Application $application, private readonly ApplicationPresenter $applicationPresenter, ) { } @@ -47,10 +44,7 @@ class TalkSlidesFormFactoryTest extends TestCase 0, new Request('foo'), ); - $presenter = $this->applicationPresenter->createUiPresenter('Admin:Talks', 'foo', 'slides'); - PrivateProperty::setValue($this->application, 'presenter', $presenter); - /** @noinspection PhpInternalEntityUsedInspection */ - $form->setParent($presenter); + $this->applicationPresenter->anchorForm($form); Assert::noError(function () use (&$result, $form): void { $result = Arrays::invoke($form->onSuccess, $form); }); diff --git a/app/tests/Form/TrainingFileFormFactoryTest.phpt b/app/tests/Form/TrainingFileFormFactoryTest.phpt index e419b2d7f..e553ae1bf 100644 --- a/app/tests/Form/TrainingFileFormFactoryTest.phpt +++ b/app/tests/Form/TrainingFileFormFactoryTest.phpt @@ -35,9 +35,7 @@ class TrainingFileFormFactoryTest extends TestCase new DateTimeImmutable(), [], ); - $presenter = $applicationPresenter->createUiPresenter('Admin:Trainings', 'foo', 'file'); - /** @noinspection PhpInternalEntityUsedInspection */ - $this->form->setParent($presenter); + $applicationPresenter->anchorForm($this->form); } diff --git a/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt b/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt index 56cf40491..e569e1b7c 100644 --- a/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt +++ b/app/tests/Form/UpcKeysSsidFormFactoryTest.phpt @@ -32,9 +32,7 @@ class UpcKeysSsidFormFactoryTest extends TestCase }, null, ); - $presenter = $applicationPresenter->createUiPresenter('UpcKeys:Homepage', 'foo', 'default'); - /** @noinspection PhpInternalEntityUsedInspection */ - $this->form->setParent($presenter); + $applicationPresenter->anchorForm($this->form); } diff --git a/app/tests/Pulse/Passwords/PasswordsTest.phpt b/app/tests/Pulse/Passwords/PasswordsTest.phpt index b10b914f9..fee718303 100644 --- a/app/tests/Pulse/Passwords/PasswordsTest.phpt +++ b/app/tests/Pulse/Passwords/PasswordsTest.phpt @@ -13,9 +13,7 @@ use MichalSpacekCz\Pulse\Passwords\Storage\StorageWildcardSite; use MichalSpacekCz\Test\Application\ApplicationPresenter; use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\DateTime\DateTimeMachineFactory; -use MichalSpacekCz\Test\PrivateProperty; use MichalSpacekCz\Test\TestCaseRunner; -use Nette\Application\Application; use Override; use Tester\Assert; use Tester\TestCase; @@ -29,7 +27,6 @@ class PasswordsTest extends TestCase public function __construct( private readonly Passwords $passwords, private readonly Database $database, - private readonly Application $application, private readonly ApplicationPresenter $applicationPresenter, private readonly PasswordsStorageAlgorithmFormFactory $formFactory, private readonly DateTimeMachineFactory $dateTimeFactory, @@ -583,10 +580,7 @@ class PasswordsTest extends TestCase }, 1, ); - $presenter = $this->applicationPresenter->createUiPresenter('Admin:Pulse', 'foo', 'slides'); - PrivateProperty::setValue($this->application, 'presenter', $presenter); - /** @noinspection PhpInternalEntityUsedInspection */ - $form->setParent($presenter); + $this->applicationPresenter->anchorForm($form); return $form; } From 14ac36e923c2660d01e7a439b27add46e77ece58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 1 Dec 2024 06:11:54 +0100 Subject: [PATCH 21/24] Rename the skip method to needsInternet as that's what it marks anyway --- app/disallowed-calls.neon | 2 +- app/src/Test/TestCaseRunner.php | 14 ++++++-------- app/tests/CompanyInfo/CompanyRegisterAresTest.phpt | 2 +- .../CompanyInfo/CompanyRegisterRegisterUzTest.phpt | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/disallowed-calls.neon b/app/disallowed-calls.neon index 202493daf..8a69ada80 100644 --- a/app/disallowed-calls.neon +++ b/app/disallowed-calls.neon @@ -23,7 +23,7 @@ parameters: method: 'Tester\Environment::skip()' message: 'use TestCaseRunner::skip() instead, it can ignore skipping with an environment variable' allowInMethods: - - 'MichalSpacekCz\Test\TestCaseRunner::skip()' + - 'MichalSpacekCz\Test\TestCaseRunner::needsInternet()' - method: - 'Nette\Utils\Strings::match()' diff --git a/app/src/Test/TestCaseRunner.php b/app/src/Test/TestCaseRunner.php index 799e1f9ca..ad335ea09 100644 --- a/app/src/Test/TestCaseRunner.php +++ b/app/src/Test/TestCaseRunner.php @@ -57,18 +57,16 @@ public static function run(string $test): void } - public static function skip(string $message): void + public static function needsInternet(): void { if (getenv(self::INCLUDE_SKIPPED_ENV_VAR_NAME) === self::INCLUDE_SKIPPED_ENV_VAR_VALUE) { return; } - Environment::skip($message); - } - - - public static function includeSkippedEnvVarUsage(): string - { - return self::INCLUDE_SKIPPED_ENV_VAR_NAME . '=' . self::INCLUDE_SKIPPED_ENV_VAR_VALUE; + Environment::skip(sprintf( + 'The test uses the Internet, to not skip the test case run it with `%s=%s`', + TestCaseRunner::INCLUDE_SKIPPED_ENV_VAR_NAME, + TestCaseRunner::INCLUDE_SKIPPED_ENV_VAR_VALUE, + )); } } diff --git a/app/tests/CompanyInfo/CompanyRegisterAresTest.phpt b/app/tests/CompanyInfo/CompanyRegisterAresTest.phpt index 4314b7953..3e421a0ed 100644 --- a/app/tests/CompanyInfo/CompanyRegisterAresTest.phpt +++ b/app/tests/CompanyInfo/CompanyRegisterAresTest.phpt @@ -31,7 +31,7 @@ class CompanyRegisterAresTest extends TestCase public function testGetDetails(): void { - TestCaseRunner::skip('The test uses the Internet, to not skip the test case run it with `' . TestCaseRunner::includeSkippedEnvVarUsage() . '`'); + TestCaseRunner::needsInternet(); $expected = new CompanyInfoDetails( 200, 'OK', diff --git a/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt b/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt index d1fa94f90..3fa4c93f5 100644 --- a/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt +++ b/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt @@ -27,7 +27,7 @@ class CompanyRegisterRegisterUzTest extends TestCase public function testGetDetails(): void { - TestCaseRunner::skip('The test uses the Internet, to not skip the test case run it with `' . TestCaseRunner::includeSkippedEnvVarUsage() . '`'); + TestCaseRunner::needsInternet(); $expected = new CompanyInfoDetails( 200, 'OK', From 9af70cc0bc3be0b21bb1ed53520b1cc94073f37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sat, 30 Nov 2024 06:49:04 +0100 Subject: [PATCH 22/24] Add assert()s and tests to test those assert()s --- app/psalm-baseline.xml | 87 ------------- app/src/Application/Routing/BlogPostRoute.php | 2 +- app/src/EasterEgg/NetteCve202015227.php | 9 +- .../EasterEgg/Presenters/NettePresenter.php | 2 +- app/src/Form/InterviewFormFactory.php | 3 - app/src/Form/TalkFormFactory.php | 3 - app/src/Form/TalkSlidesFormFactory.php | 5 +- ...iningApplicationPreliminaryFormFactory.php | 4 +- app/src/Form/TrainingReviewFormFactory.php | 24 ++-- app/src/Media/VideoThumbnails.php | 15 +-- app/src/Net/DnsResolver.php | 7 +- app/src/Talks/Slides/TalkSlides.php | 13 +- .../TrainingApplicationFormDataLogger.php | 8 +- .../TrainingApplicationFormSpam.php | 16 +-- .../TrainingApplicationFormSuccess.php | 27 ++-- app/src/Training/Reviews/TrainingReviews.php | 5 +- .../EasterEgg/NetteCve202015227Test.phpt | 27 ++-- .../Form/TrainingReviewFormFactoryTest.phpt | 119 ++++++++++++++++++ app/tests/Net/DnsResolverTest.phpt | 34 +++++ ...TrainingApplicationFormDataLoggerTest.phpt | 26 ++-- .../TrainingApplicationFormSpamTest.phpt | 98 ++++++++++----- 21 files changed, 341 insertions(+), 193 deletions(-) create mode 100644 app/tests/Form/TrainingReviewFormFactoryTest.phpt create mode 100644 app/tests/Net/DnsResolverTest.phpt diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index e41232884..6cb0b3179 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -117,11 +117,6 @@ videoHref]]> - - - deleteReplaced]]> - - @@ -161,12 +156,6 @@ status]]> - - - email]]> - name]]> - - @@ -226,26 +215,6 @@ - - - company]]> - company]]> - hidden]]> - hidden]]> - href ?: null]]> - href ?: null]]> - jobTitle ?: null]]> - jobTitle ?: null]]> - name]]> - name]]> - note ?: null]]> - note ?: null]]> - ranking ?: null]]> - ranking ?: null]]> - review]]> - review]]> - - @@ -254,21 +223,9 @@ videoThumbnail]]> - videoThumbnail]]> - videoThumbnailAlternative]]> videoThumbnailAlternative]]> - - - - - - - - - - @@ -278,15 +235,6 @@ filename ?? '']]> filenameAlternative ?? '']]> - alias]]> - number]]> - number]]> - replace]]> - replace]]> - replaceAlternative]]> - replaceAlternative]]> - speakerNotes]]> - title]]> @@ -294,41 +242,6 @@ - - - city]]> - city]]> - city]]> - company]]> - company]]> - company]]> - companyId]]> - companyId]]> - companyId]]> - companyTaxId]]> - companyTaxId]]> - companyTaxId]]> - country]]> - country]]> - country]]> - email]]> - email]]> - email]]> - name]]> - name]]> - name]]> - note]]> - note]]> - note]]> - street]]> - street]]> - street]]> - trainingId]]> - zip]]> - zip]]> - zip]]> - - ranking]]> diff --git a/app/src/Application/Routing/BlogPostRoute.php b/app/src/Application/Routing/BlogPostRoute.php index daf0afa86..21c1addf0 100644 --- a/app/src/Application/Routing/BlogPostRoute.php +++ b/app/src/Application/Routing/BlogPostRoute.php @@ -31,7 +31,7 @@ public function __construct( * Maps HTTP request to a Request object. * * @param IRequest $httpRequest - * @return array|null + * @return array|null */ #[Override] public function match(IRequest $httpRequest): ?array diff --git a/app/src/EasterEgg/NetteCve202015227.php b/app/src/EasterEgg/NetteCve202015227.php index 124130ac5..490589f23 100644 --- a/app/src/EasterEgg/NetteCve202015227.php +++ b/app/src/EasterEgg/NetteCve202015227.php @@ -5,6 +5,7 @@ use Nette\Application\BadRequestException; use Nette\Application\Routers\RouteList; +use Nette\Application\UI\Component; /** * Nette CVE-2020-15227, here to easter-egg some bots @@ -19,10 +20,7 @@ class NetteCve202015227 { - /** - * @param array $params - */ - public function rce(string $callback, array $params): NetteCve202015227Rce + public function rce(string $callback, Component $component): NetteCve202015227Rce { $callback = strtolower($callback); $paramNames = [ @@ -40,7 +38,8 @@ public function rce(string $callback, array $params): NetteCve202015227Rce $data = []; - $param = $params[$paramNames[$callback]] ?? null; + $param = $component->getParameters()[$paramNames[$callback]] ?? null; + assert(is_string($param) || $param === null); if ($param === null) { throw new BadRequestException(sprintf("[%s] Empty param '%s' for callback '%s'", __CLASS__, $paramNames[$callback], $callback)); } diff --git a/app/src/EasterEgg/Presenters/NettePresenter.php b/app/src/EasterEgg/Presenters/NettePresenter.php index 2d000de53..2ef6f7288 100644 --- a/app/src/EasterEgg/Presenters/NettePresenter.php +++ b/app/src/EasterEgg/Presenters/NettePresenter.php @@ -23,7 +23,7 @@ public function __construct( public function actionMicro(string $callback): void { sleep(random_int(5, 20)); - $rce = $this->cve202015227->rce($callback, $this->getParameters()); + $rce = $this->cve202015227->rce($callback, $this); $this->setView($rce->view->value); $this->template->eth0RxPackets = $rce->eth0RxPackets; $this->template->eth1RxPackets = $rce->eth1RxPackets; diff --git a/app/src/Form/InterviewFormFactory.php b/app/src/Form/InterviewFormFactory.php index 43ddba884..370895ac9 100644 --- a/app/src/Form/InterviewFormFactory.php +++ b/app/src/Form/InterviewFormFactory.php @@ -121,9 +121,6 @@ public function create(callable $onSuccess, ?Interview $interview = null): UiFor } $onSuccess(); }; - - $this->videoThumbnails->addOnValidateUploads($form, $videoThumbnailFormFields); - return $form; } diff --git a/app/src/Form/TalkFormFactory.php b/app/src/Form/TalkFormFactory.php index a024d855a..e1cfd53ba 100644 --- a/app/src/Form/TalkFormFactory.php +++ b/app/src/Form/TalkFormFactory.php @@ -182,9 +182,6 @@ public function create(callable $onSuccess, ?Talk $talk = null): UiForm $message->addHtml(Html::el('a')->href($this->linkGenerator->link('Www:Talks:talk', [$values->action]))->setText('Zobrazit')); $onSuccess($message); }; - - $this->videoThumbnails->addOnValidateUploads($form, $videoThumbnailFormFields); - return $form; } diff --git a/app/src/Form/TalkSlidesFormFactory.php b/app/src/Form/TalkSlidesFormFactory.php index ad3c4c0a8..12bb0e9d7 100644 --- a/app/src/Form/TalkSlidesFormFactory.php +++ b/app/src/Form/TalkSlidesFormFactory.php @@ -11,6 +11,7 @@ use Nette\Application\Request; use Nette\Forms\Container; use Nette\Forms\Form; +use Nette\Utils\ArrayHash; use Nette\Utils\Html; readonly class TalkSlidesFormFactory @@ -61,6 +62,9 @@ public function create(callable $onSuccess, int $talkId, TalkSlideCollection $sl $form->onSuccess[] = function (UiForm $form) use ($slides, $onSuccess, $talkId): void { try { $values = $form->getFormValues(); + 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); $message = $this->texyFormatter->translate('messages.talks.admin.slideadded'); $type = 'info'; @@ -96,7 +100,6 @@ private function addSlideFields(UiForm $form, Container $container, ?int $filena ->setRequired('Zadejte prosím alias') ->addRule(Form::Pattern, 'Alias musí být ve formátu [_.,a-z0-9-]+', '[_.,a-z0-9-]+'); $container->addInteger('number', 'Slajd:') - ->setHtmlType('number') ->setDefaultValue(1) ->setHtmlAttribute('class', 'right slide-nr') ->setRequired('Zadejte prosím číslo slajdu'); diff --git a/app/src/Form/TrainingApplicationPreliminaryFormFactory.php b/app/src/Form/TrainingApplicationPreliminaryFormFactory.php index 2c2d8342b..b302c98c2 100644 --- a/app/src/Form/TrainingApplicationPreliminaryFormFactory.php +++ b/app/src/Form/TrainingApplicationPreliminaryFormFactory.php @@ -27,8 +27,10 @@ public function create(callable $onSuccess, callable $onError, int $trainingId, $form->addSubmit('signUp', 'Odeslat'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess, $onError, $trainingId, $action): void { $values = $form->getFormValues(); + assert(is_string($values->name)); + assert(is_string($values->email)); try { - $this->formSpam->check($values); + $this->formSpam->check($values->name); $this->trainingApplicationStorage->addPreliminaryInvitation($trainingId, $values->name, $values->email); $onSuccess($action); } catch (SpammyApplicationException) { diff --git a/app/src/Form/TrainingReviewFormFactory.php b/app/src/Form/TrainingReviewFormFactory.php index 22d1c6ce6..379d7b67b 100644 --- a/app/src/Form/TrainingReviewFormFactory.php +++ b/app/src/Form/TrainingReviewFormFactory.php @@ -51,9 +51,9 @@ public function create(callable $onSuccess, int $dateId, ?TrainingReview $review ->setRequired(false) ->addRule(Form::MaxLength, 'Maximální délka odkazu je %d znaků', 200); $form->addCheckbox('hidden', 'Skrýt:'); - $form->addText('ranking', 'Pořadí:') + $form->addInteger('ranking', 'Pořadí:') ->setRequired(false) - ->setHtmlType('number'); + ->addRule(Form::Min, 'Minimální hodnota pořadí je %d', 0); $form->addText('note', 'Poznámka:') ->setRequired(false) ->addRule(Form::MaxLength, 'Maximální délka poznámky je %d znaků', 2000); @@ -64,17 +64,25 @@ public function create(callable $onSuccess, int $dateId, ?TrainingReview $review $form->onSuccess[] = function (UiForm $form) use ($onSuccess, $review, $dateId): void { $values = $form->getFormValues(); + assert(is_string($values->name)); + assert(is_string($values->company)); + assert(is_string($values->jobTitle)); + assert(is_string($values->review)); + assert(is_string($values->href)); + assert(is_bool($values->hidden)); + assert(is_int($values->ranking) || $values->ranking === null); + assert(is_string($values->note)); if ($review) { $this->trainingReviews->updateReview( $review->getId(), $dateId, $values->name, $values->company, - $values->jobTitle ?: null, + $values->jobTitle !== '' ? $values->jobTitle : null, $values->review, - $values->href ?: null, + $values->href !== '' ? $values->href : null, $values->hidden, - $values->ranking ?: null, + $values->ranking !== 0 ? $values->ranking : null, $values->note ?: null, ); } else { @@ -82,11 +90,11 @@ public function create(callable $onSuccess, int $dateId, ?TrainingReview $review $dateId, $values->name, $values->company, - $values->jobTitle ?: null, + $values->jobTitle !== '' ? $values->jobTitle : null, $values->review, - $values->href ?: null, + $values->href !== '' ? $values->href : null, $values->hidden, - $values->ranking ?: null, + $values->ranking !== 0 ? $values->ranking : null, $values->note ?: null, ); } diff --git a/app/src/Media/VideoThumbnails.php b/app/src/Media/VideoThumbnails.php index ec2b2b7fb..c01237e1d 100644 --- a/app/src/Media/VideoThumbnails.php +++ b/app/src/Media/VideoThumbnails.php @@ -71,17 +71,14 @@ public function addFormFields(UiForm $form, bool $hasMainVideoThumbnail, bool $h $videoThumbnailAlternative->addCondition(Form::Filled, true) ->toggle('#currentVideoThumbnailAlternative', false); } - return new VideoThumbnailFileUploads($videoThumbnail, $videoThumbnailAlternative, $hasMainVideoThumbnail, $hasAlternativeVideoThumbnail); - } - - - public function addOnValidateUploads(UiForm $form, VideoThumbnailFileUploads $formFields): void - { - $form->onValidate[] = function (UiForm $form) use ($formFields): void { + $form->onValidate[] = function (UiForm $form) use ($videoThumbnail, $videoThumbnailAlternative): void { $values = $form->getFormValues(); - $this->validateUpload($values->videoThumbnail, $formFields->getVideoThumbnail()); - $this->validateUpload($values->videoThumbnailAlternative, $formFields->getVideoThumbnailAlternative()); + assert($values->videoThumbnail instanceof FileUpload); + assert($values->videoThumbnailAlternative instanceof FileUpload); + $this->validateUpload($values->videoThumbnail, $videoThumbnail); + $this->validateUpload($values->videoThumbnailAlternative, $videoThumbnailAlternative); }; + return new VideoThumbnailFileUploads($videoThumbnail, $videoThumbnailAlternative, $hasMainVideoThumbnail, $hasAlternativeVideoThumbnail); } diff --git a/app/src/Net/DnsResolver.php b/app/src/Net/DnsResolver.php index f2a7b1f8f..675ec74d5 100644 --- a/app/src/Net/DnsResolver.php +++ b/app/src/Net/DnsResolver.php @@ -23,7 +23,12 @@ public function getRecords(string $hostname, int $type): array } $result = []; foreach ($records as $record) { - $result[] = new DnsRecord(...$record); + assert(is_string($record['host'])); + assert(is_string($record['class'])); + assert(is_int($record['ttl'])); + assert(is_string($record['type'])); + assert(is_string($record['ip'])); + $result[] = new DnsRecord($record['host'], $record['class'], $record['ttl'], $record['type'], $record['ip']); } return $result; } diff --git a/app/src/Talks/Slides/TalkSlides.php b/app/src/Talks/Slides/TalkSlides.php index 7dda00862..5d90d4f6d 100644 --- a/app/src/Talks/Slides/TalkSlides.php +++ b/app/src/Talks/Slides/TalkSlides.php @@ -217,11 +217,14 @@ private function addSlides(int $talkId, array $slides): void $lastNumber = 0; try { foreach ($slides as $slide) { + assert($slide->replace instanceof FileUpload); + assert($slide->replaceAlternative instanceof FileUpload); + assert(is_int($slide->number)); $width = self::SLIDE_MAX_WIDTH; $height = self::SLIDE_MAX_HEIGHT; $replace = $this->replaceSlideImage($talkId, $slide->replace, $this->supportedImageFileFormats->getMainExtensionByContentType(...), false, null, $width, $height); $replaceAlternative = $this->replaceSlideImage($talkId, $slide->replaceAlternative, $this->supportedImageFileFormats->getAlternativeExtensionByContentType(...), false, null, $width, $height); - $lastNumber = (int)$slide->number; + $lastNumber = $slide->number; $this->database->query( 'INSERT INTO talk_slides', [ @@ -259,6 +262,14 @@ private function updateSlides(int $talkId, TalkSlideCollection $originalSlides, } } foreach ($slides as $id => $slide) { + assert($slide->replace instanceof FileUpload || $slide->replace === null); + assert($slide->replaceAlternative instanceof FileUpload || $slide->replaceAlternative === null); + assert(is_string($slide->alias)); + assert(is_int($slide->number)); + assert(is_string($slide->filename)); + assert(is_string($slide->filenameAlternative)); + assert(is_string($slide->title)); + assert(is_string($slide->speakerNotes)); $width = self::SLIDE_MAX_WIDTH; $height = self::SLIDE_MAX_HEIGHT; diff --git a/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php b/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php index ab8548a54..ca5c94e23 100644 --- a/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php +++ b/app/src/Training/ApplicationForm/TrainingApplicationFormDataLogger.php @@ -4,18 +4,20 @@ namespace MichalSpacekCz\Training\ApplicationForm; use MichalSpacekCz\Training\Applications\TrainingApplicationSessionSection; -use stdClass; use Tracy\Debugger; class TrainingApplicationFormDataLogger { - public function log(stdClass $values, string $name, int $dateId, ?TrainingApplicationSessionSection $sessionSection): void + /** + * @param array $values + */ + public function log(array $values, string $name, int $dateId, ?TrainingApplicationSessionSection $sessionSection): void { $applicationId = $sessionSection?->getApplicationIdByDateId($name, $dateId); $logSession = $applicationId !== null ? "id => '{$applicationId}', dateId => '{$dateId}'" : null; $logValues = []; - foreach ((array)$values as $key => $value) { + foreach ($values as $key => $value) { $logValues[] = sprintf('%s => %s', $key, is_string($value) ? "'{$value}'" : get_debug_type($value)); } $message = sprintf( diff --git a/app/src/Training/ApplicationForm/TrainingApplicationFormSpam.php b/app/src/Training/ApplicationForm/TrainingApplicationFormSpam.php index 953658619..8c4ff3115 100644 --- a/app/src/Training/ApplicationForm/TrainingApplicationFormSpam.php +++ b/app/src/Training/ApplicationForm/TrainingApplicationFormSpam.php @@ -5,7 +5,6 @@ use Composer\Pcre\Regex; use MichalSpacekCz\Training\Exceptions\SpammyApplicationException; -use stdClass; class TrainingApplicationFormSpam { @@ -16,15 +15,18 @@ class TrainingApplicationFormSpam private const string FIELD_MISSING_VALUE = 'missing'; - public function check(stdClass $values): void + /** + * @throws SpammyApplicationException + */ + public function check(string $name, ?string $company = null, ?string $companyId = null, ?string $companyTaxId = null, ?string $note = null): void { - if (Regex::isMatch('~\s+href="\s*https?://~', $values->note ?? '')) { + if (Regex::isMatch('~\s+href="\s*https?://~', $note ?? self::FIELD_MISSING_VALUE)) { throw new SpammyApplicationException(); } elseif ( - ctype_lower($values->name ?? self::FIELD_MISSING_VALUE) - && ctype_lower($values->company ?? self::FIELD_MISSING_VALUE) - && ctype_lower($values->companyId ?? self::FIELD_MISSING_VALUE) - && ctype_lower($values->companyTaxId ?? self::FIELD_MISSING_VALUE) + ctype_lower($name) + && ctype_lower($company ?? self::FIELD_MISSING_VALUE) + && ctype_lower($companyId ?? self::FIELD_MISSING_VALUE) + && ctype_lower($companyTaxId ?? self::FIELD_MISSING_VALUE) ) { throw new SpammyApplicationException(); } diff --git a/app/src/Training/ApplicationForm/TrainingApplicationFormSuccess.php b/app/src/Training/ApplicationForm/TrainingApplicationFormSuccess.php index a853595ad..1bd19f185 100644 --- a/app/src/Training/ApplicationForm/TrainingApplicationFormSuccess.php +++ b/app/src/Training/ApplicationForm/TrainingApplicationFormSuccess.php @@ -21,7 +21,6 @@ use ParagonIE\Halite\Alerts\HaliteAlert; use PDOException; use SodiumException; -use stdClass; use Tracy\Debugger; readonly class TrainingApplicationFormSuccess @@ -57,11 +56,22 @@ public function success( TrainingApplicationSessionSection $sessionSection, ): void { $values = $form->getFormValues(); + assert(is_string($values->name)); + assert(is_string($values->email)); + assert(is_string($values->company)); + assert(is_string($values->street)); + assert(is_string($values->city)); + assert(is_string($values->zip)); + assert(is_string($values->country)); + assert(is_string($values->companyId)); + assert(is_string($values->companyTaxId)); + assert(is_string($values->note)); try { - $this->formSpam->check($values); + $this->formSpam->check($values->name, $values->company, $values->companyId, $values->companyTaxId, $values->note); if ($multipleDates) { - $this->checkTrainingDate($values, $action, $dates, $sessionSection); - $date = $dates[$values->trainingId] ?? false; + assert(is_int($values->trainingId)); + $this->checkTrainingDate((array)$values, $action, $values->trainingId, $dates, $sessionSection); + $date = $dates[$values->trainingId]; } else { $date = reset($dates); } @@ -153,14 +163,15 @@ public function success( /** + * @param array $values * @param array $dates * @throws TrainingDateNotUpcomingException */ - private function checkTrainingDate(stdClass $values, string $name, array $dates, TrainingApplicationSessionSection $sessionSection): void + private function checkTrainingDate(array $values, string $name, int $dateId, array $dates, TrainingApplicationSessionSection $sessionSection): void { - if (!isset($dates[$values->trainingId])) { - $this->formDataLogger->log($values, $name, $values->trainingId, $sessionSection); - throw new TrainingDateNotUpcomingException($values->trainingId, $dates); + if (!isset($dates[$dateId])) { + $this->formDataLogger->log($values, $name, $dateId, $sessionSection); + throw new TrainingDateNotUpcomingException($dateId, $dates); } } diff --git a/app/src/Training/Reviews/TrainingReviews.php b/app/src/Training/Reviews/TrainingReviews.php index ced5fd2a6..a2e3fa675 100644 --- a/app/src/Training/Reviews/TrainingReviews.php +++ b/app/src/Training/Reviews/TrainingReviews.php @@ -3,8 +3,8 @@ namespace MichalSpacekCz\Training\Reviews; -use DateTime; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use MichalSpacekCz\Formatter\TexyFormatter; use MichalSpacekCz\Training\Exceptions\TrainingReviewNotFoundException; use Nette\Database\Explorer; @@ -17,6 +17,7 @@ public function __construct( private Explorer $database, private TypedDatabase $typedDatabase, private TexyFormatter $texyFormatter, + private DateTimeFactory $dateTimeFactory, ) { } @@ -176,7 +177,7 @@ public function updateReview(int $reviewId, int $dateId, string $name, string $c public function addReview(int $dateId, string $name, string $company, ?string $jobTitle, string $review, ?string $href, bool $hidden, ?int $ranking, ?string $note): void { - $datetime = new DateTime(); + $datetime = $this->dateTimeFactory->create(); $timeZone = $datetime->getTimezone()->getName(); $this->database->query( 'INSERT INTO training_reviews ?', diff --git a/app/tests/EasterEgg/NetteCve202015227Test.phpt b/app/tests/EasterEgg/NetteCve202015227Test.phpt index 51258e996..adad7d089 100644 --- a/app/tests/EasterEgg/NetteCve202015227Test.phpt +++ b/app/tests/EasterEgg/NetteCve202015227Test.phpt @@ -7,6 +7,7 @@ use MichalSpacekCz\Test\TestCaseRunner; use Nette\Application\BadRequestException; use Nette\Application\Routers\Route; use Nette\Application\Routers\RouteList; +use Nette\Application\UI\Component; use Tester\Assert; use Tester\TestCase; @@ -25,7 +26,7 @@ class NetteCve202015227Test extends TestCase public function testRceUnknownCallback(): void { Assert::exception(function (): void { - $this->cve202015227->rce('foo', ['bar' => 'baz']); + $this->cve202015227->rce('foo', $this->createComponent(['bar' => 'baz'])); }, BadRequestException::class, "[MichalSpacekCz\EasterEgg\NetteCve202015227] Unknown callback 'foo'"); } @@ -33,7 +34,7 @@ class NetteCve202015227Test extends TestCase public function testRceEmptyParam(): void { Assert::exception(function (): void { - $this->cve202015227->rce('exec', ['bar' => 'baz']); + $this->cve202015227->rce('exec', $this->createComponent(['bar' => 'baz'])); }, BadRequestException::class, "[MichalSpacekCz\EasterEgg\NetteCve202015227] Empty param 'command' for callback 'exec'"); } @@ -41,21 +42,21 @@ class NetteCve202015227Test extends TestCase public function testRceUnknownValue(): void { Assert::exception(function (): void { - $this->cve202015227->rce('exec', ['command' => 'baz']); + $this->cve202015227->rce('exec', $this->createComponent(['command' => 'baz'])); }, BadRequestException::class, "[MichalSpacekCz\EasterEgg\NetteCve202015227] Unknown value 'baz' for callback 'exec' and param 'command'"); } public function testRceLs(): void { - $rce = $this->cve202015227->rce('exec', ['command' => 'ls foo']); + $rce = $this->cve202015227->rce('exec', $this->createComponent(['command' => 'ls foo'])); Assert::same(NetteCve202015227View::Ls, $rce->view); } public function testRceIfconfig(): void { - $rce = $this->cve202015227->rce('exec', ['command' => 'ifconfig bar']); + $rce = $this->cve202015227->rce('exec', $this->createComponent(['command' => 'ifconfig bar'])); Assert::same(NetteCve202015227View::Ifconfig, $rce->view); Assert::type('string', $rce->eth0RxPackets); Assert::type('string', $rce->eth1RxPackets); @@ -74,7 +75,7 @@ class NetteCve202015227Test extends TestCase public function testRceWget(): void { - $rce = $this->cve202015227->rce('shell_exec', ['cmd' => 'wget example.com']); + $rce = $this->cve202015227->rce('shell_exec', $this->createComponent(['cmd' => 'wget example.com'])); Assert::same(NetteCve202015227View::Wget, $rce->view); } @@ -82,7 +83,7 @@ class NetteCve202015227Test extends TestCase /** @dataProvider getCommands */ public function testRceNotFound(NetteCve202015227View $view, string $command, string $cmd): void { - $rce = $this->cve202015227->rce('shell_exec', ['cmd' => $cmd]); + $rce = $this->cve202015227->rce('shell_exec', $this->createComponent(['cmd' => $cmd])); Assert::same($view, $rce->view); Assert::same($command, $rce->command); } @@ -141,6 +142,18 @@ class NetteCve202015227Test extends TestCase ]; } + + /** + * @param array $params + */ + private function createComponent(array $params): Component + { + $component = new class extends Component { + }; + $component->loadState($params); + return $component; + } + } TestCaseRunner::run(NetteCve202015227Test::class); diff --git a/app/tests/Form/TrainingReviewFormFactoryTest.phpt b/app/tests/Form/TrainingReviewFormFactoryTest.phpt new file mode 100644 index 000000000..d97968f7c --- /dev/null +++ b/app/tests/Form/TrainingReviewFormFactoryTest.phpt @@ -0,0 +1,119 @@ +setDateTime(new DateTimeImmutable('2020-01-01 12:34:56')); + } + + + #[Override] + protected function tearDown(): void + { + $this->database->reset(); + $this->resultDateId = null; + } + + + public function testCreateOnSuccessAdd(): void + { + $form = $this->formFactory->create( + function (int $dateId): void { + $this->resultDateId = $dateId; + }, + self::DATE_ID, + null, + ); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same(self::DATE_ID, $this->resultDateId); + Assert::same([ + [ + 'key_date' => self::DATE_ID, + 'name' => '', + 'company' => '', + 'job_title' => null, + 'review' => '', + 'href' => null, + 'added' => '2020-01-01 12:34:56', + 'added_timezone' => 'Europe/Prague', + 'hidden' => false, + 'ranking' => null, + 'note' => null, + ], + ], $this->database->getParamsArrayForQuery('INSERT INTO training_reviews ?')); + } + + + public function testCreateOnSuccessEdit(): void + { + $form = $this->formFactory->create( + function (int $dateId): void { + $this->resultDateId = $dateId; + }, + self::DATE_ID, + new TrainingReview( + 303, + 'John Deere', + 'Comp Any', + 'Team Le-ad', + Html::fromHtml('foo'), + '**foo**', + 'https://example.com', + false, + 3, + 'No tea', + self::DATE_ID, + ), + ); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same(self::DATE_ID, $this->resultDateId); + Assert::same([ + [ + 'key_date' => self::DATE_ID, + 'name' => 'John Deere', + 'company' => 'Comp Any', + 'job_title' => 'Team Le-ad', + 'review' => '**foo**', + 'href' => 'https://example.com', + 'hidden' => false, + 'ranking' => 3, + 'note' => 'No tea', + ], + ], $this->database->getParamsArrayForQuery('UPDATE training_reviews SET ? WHERE id_review = ?')); + } + +} + +TestCaseRunner::run(TrainingReviewFormFactoryTest::class); diff --git a/app/tests/Net/DnsResolverTest.phpt b/app/tests/Net/DnsResolverTest.phpt new file mode 100644 index 000000000..48f744da8 --- /dev/null +++ b/app/tests/Net/DnsResolverTest.phpt @@ -0,0 +1,34 @@ +dnsResolver->getRecords('one.one.one.one', DNS_A); + $ips = array_map(fn(DnsRecord $dnsRecord): ?string => $dnsRecord->getIp(), $records); + sort($ips); + Assert::same(['1.0.0.1', '1.1.1.1'], $ips); + } + +} + +TestCaseRunner::run(DnsResolverTest::class); diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt index 0b4b25a66..bc6708e8e 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt @@ -15,7 +15,6 @@ use MichalSpacekCz\Training\Files\TrainingFiles; use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; use Nette\Utils\Html; use Override; -use stdClass; use Tester\Assert; use Tester\TestCase; @@ -50,16 +49,17 @@ class TrainingApplicationFormDataLoggerTest extends TestCase public function testLogNoValuesNoSession(): void { - $this->formDataLogger->log(new stdClass(), 'foo', self::DATE_ID, null); + $this->formDataLogger->log([], 'foo', self::DATE_ID, null); Assert::same(['Application session data for foo: undefined, form values: empty'], $this->logger->getLogged()); } public function testLogNoSession(): void { - $values = new stdClass(); - $values->key1 = 'value1'; - $values->key2 = 'value2'; + $values = [ + 'key1' => 'value1', + 'key2' => 'value2', + ]; $this->formDataLogger->log($values, 'foo', self::DATE_ID, null); Assert::same(["Application session data for foo: undefined, form values: key1 => 'value1', key2 => 'value2'"], $this->logger->getLogged()); } @@ -67,9 +67,10 @@ class TrainingApplicationFormDataLoggerTest extends TestCase public function testLogEmptySession(): void { - $values = new stdClass(); - $values->key1 = 'value1'; - $values->key2 = 'value2'; + $values = [ + 'key1' => 'value1', + 'key2' => 'value2', + ]; $this->formDataLogger->log($values, 'foo', self::DATE_ID, $this->getTrainingSessionSection()); Assert::same(["Application session data for foo: empty, form values: key1 => 'value1', key2 => 'value2'"], $this->logger->getLogged()); } @@ -77,10 +78,11 @@ class TrainingApplicationFormDataLoggerTest extends TestCase public function testLog(): void { - $values = new stdClass(); - $values->key1 = 'value1'; - $values->key2 = 'value2'; - $values->key3 = 1336; + $values = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 1336, + ]; $trainingName = 'foo'; $session = $this->getTrainingSessionSection(); diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormSpamTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormSpamTest.phpt index da7ceb0fa..15edab45c 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormSpamTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormSpamTest.phpt @@ -5,11 +5,9 @@ declare(strict_types = 1); namespace MichalSpacekCz\Training\ApplicationForm; -use Generator; use MichalSpacekCz\Test\NullLogger; use MichalSpacekCz\Test\TestCaseRunner; use MichalSpacekCz\Training\Exceptions\SpammyApplicationException; -use stdClass; use Tester\Assert; use Tester\TestCase; @@ -26,45 +24,79 @@ class TrainingApplicationFormSpamTest extends TestCase } - public function getValues(): Generator + /** + * @return list + */ + public function getValues(): array { - $values = new stdClass(); - $values->note = 'foo href="https:// example" bar baz'; - yield [$values, false]; - - $values = new stdClass(); - $values->name = 'zggnbijhah'; - $values->companyId = 'vwetyeofcx'; - $values->companyTaxId = 'tyqvukaims'; - $values->company = 'qzpormrfcq'; - yield [$values, false]; - - $values = new stdClass(); - $values->name = 'zggnbijhah'; - yield [$values, false]; - - yield [new stdClass(), false]; - $values = new stdClass(); - $values->name = 'foo bar'; - yield [$values, true]; - - $values = new stdClass(); - $values->companyId = 'foobar1'; - yield [$values, true]; - - $values = new stdClass(); - $values->companyTaxId = 'foobar1'; - yield [$values, true]; + return [ + [ + 'name' => 'foo bar', + 'companyId' => null, + 'companyTaxId' => null, + 'company' => null, + 'note' => 'foo href="https:// example" bar baz', + 'isNice' => false, + ], + [ + 'name' => 'zggnbijhah', + 'companyId' => 'vwetyeofcx', + 'companyTaxId' => 'tyqvukaims', + 'company' => 'qzpormrfcq', + 'note' => null, + 'isNice' => false, + ], + [ + 'name' => 'zggnbijhah', + 'companyId' => null, + 'companyTaxId' => null, + 'company' => null, + 'note' => null, + 'isNice' => false, + ], + [ + 'name' => 'foo bar', + 'companyId' => null, + 'companyTaxId' => null, + 'company' => null, + 'note' => null, + 'isNice' => true, + ], + [ + 'name' => '', + 'companyId' => 'foobar1', + 'companyTaxId' => null, + 'company' => null, + 'note' => null, + 'isNice' => true, + ], + [ + 'name' => '', + 'companyId' => null, + 'companyTaxId' => 'foobar1', + 'company' => null, + 'note' => null, + 'isNice' => true, + ], + [ + 'name' => '', + 'companyId' => null, + 'companyTaxId' => null, + 'company' => 'comp any', + 'note' => null, + 'isNice' => true, + ], + ]; } /** * @dataProvider getValues */ - public function testIsSpam(stdClass $values, bool $isNice): void + public function testIsSpam(string $name, ?string $companyId, ?string $companyTaxId, ?string $company, ?string $note, bool $isNice): void { - $check = function () use ($values): void { - $this->formSpam->check($values); + $check = function () use ($name, $company, $companyId, $companyTaxId, $note): void { + $this->formSpam->check($name, $company, $companyId, $companyTaxId, $note); }; if ($isNice) { Assert::noError($check); From b0e83f1a4fc35f86623a1e4fd4f218a5f32005de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 1 Dec 2024 17:53:25 +0100 Subject: [PATCH 23/24] =?UTF-8?q?Validate=20Register=20=C3=BA=C4=8Dtovn?= =?UTF-8?q?=C3=BDch=20z=C3=A1vierok=20API=20responses=20with=20nette/schem?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to how it's done with ARES. --- app/psalm-baseline.xml | 10 ---- .../CompanyInfo/CompanyRegisterRegisterUz.php | 52 +++++++++++++++---- .../CompanyRegisterRegisterUzTest.phpt | 9 ++-- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 6cb0b3179..ef9f86578 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -1,15 +1,5 @@ - - - ico]]> - mesto]]> - nazovUJ]]> - psc]]> - ulica]]> - id]]> - - getValue()]]> diff --git a/app/src/CompanyInfo/CompanyRegisterRegisterUz.php b/app/src/CompanyInfo/CompanyRegisterRegisterUz.php index 9273b418b..440db3169 100644 --- a/app/src/CompanyInfo/CompanyRegisterRegisterUz.php +++ b/app/src/CompanyInfo/CompanyRegisterRegisterUz.php @@ -9,6 +9,10 @@ use MichalSpacekCz\Http\Client\HttpClientRequest; use MichalSpacekCz\Http\Exceptions\HttpClientRequestException; use Nette\Http\IResponse; +use Nette\Schema\Elements\Structure; +use Nette\Schema\Expect; +use Nette\Schema\Processor; +use Nette\Schema\ValidationException; use Nette\Utils\Json; use Nette\Utils\JsonException; use Override; @@ -28,6 +32,7 @@ public function __construct( + private Processor $schemaProcessor, private HttpClient $httpClient, ) { } @@ -54,17 +59,46 @@ public function getDetails(string $companyId): CompanyInfoDetails if (empty($units->id)) { throw new CompanyNotFoundException(); } - $unit = $this->call('uctovna-jednotka', ['id' => reset($units->id)]); - + try { + /** @var Structure $expectArray */ + $expectArray = $this->schemaProcessor->process( + Expect::type(Structure::class), + Expect::array([ + Expect::int()->required(), + ]), + ); + $schema = Expect::structure([ + 'id' => $expectArray->otherItems()->required(), + ])->otherItems(); + /** @var object{id:array{0:int}} $data */ + $data = $this->schemaProcessor->process($schema, $units); + } catch (ValidationException $e) { + throw new CompanyInfoException($e->getMessage(), previous: $e); + } + $unit = $this->call('uctovna-jednotka', ['id' => $data->id[0]]); + try { + $schema = Expect::structure([ + 'ico' => Expect::string()->required(), + 'dic' => Expect::string(), + 'nazovUJ' => Expect::string()->required(), + 'ulica' => Expect::string()->required(), + 'mesto' => Expect::string()->required(), + 'psc' => Expect::string()->required(), + ])->otherItems(); + /** @var object{ico:string, dic?:string, nazovUJ:string, ulica:string, mesto:string, psc:string} $data */ + $data = $this->schemaProcessor->process($schema, $unit); + } catch (ValidationException $e) { + throw new CompanyInfoException($e->getMessage(), previous: $e); + } return new CompanyInfoDetails( IResponse::S200_OK, 'OK', - $unit->ico, - isset($unit->dic) && is_string($unit->dic) ? strtoupper(self::COUNTRY_CODE) . $unit->dic : '', - $unit->nazovUJ, - $unit->ulica, - $unit->mesto, - $unit->psc, + $data->ico, + isset($data->dic) ? strtoupper(self::COUNTRY_CODE) . $data->dic : '', + $data->nazovUJ, + $data->ulica, + $data->mesto, + $data->psc, self::COUNTRY_CODE, ); } @@ -72,7 +106,7 @@ public function getDetails(string $companyId): CompanyInfoDetails /** * @param string $method - * @param array $parameters + * @param array $parameters * @return stdClass JSON object * @throws CompanyInfoException */ diff --git a/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt b/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt index 3fa4c93f5..2ff1b6afc 100644 --- a/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt +++ b/app/tests/CompanyInfo/CompanyRegisterRegisterUzTest.phpt @@ -7,6 +7,7 @@ namespace MichalSpacekCz\CompanyInfo; use MichalSpacekCz\CompanyInfo\Exceptions\CompanyNotFoundException; use MichalSpacekCz\Http\Client\HttpClient; use MichalSpacekCz\Test\TestCaseRunner; +use Nette\Schema\Processor; use Tester\Assert; use Tester\TestCase; @@ -19,9 +20,11 @@ class CompanyRegisterRegisterUzTest extends TestCase private readonly CompanyRegisterRegisterUz $registerUz; - public function __construct() - { - $this->registerUz = new CompanyRegisterRegisterUz(new HttpClient()); + public function __construct( + Processor $schemaProcessor, + ) { + // Need a real HttpClient, not the mock one used in other tests + $this->registerUz = new CompanyRegisterRegisterUz($schemaProcessor, new HttpClient()); } From 404e8a27eb534ca7ab25c31394d47e9139f293a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 2 Dec 2024 04:20:33 +0100 Subject: [PATCH 24/24] Adding assert() to forms, required a bit of refactoring here and there --- app/psalm-baseline.xml | 78 --------------- app/src/Form/InterviewFormFactory.php | 22 ++++- app/src/Form/SignInHoneypotFormFactory.php | 2 + app/src/Form/TalkFormFactory.php | 33 ++++++- app/src/Form/TalkSlidesFormFactory.php | 2 +- app/src/Media/VideoThumbnails.php | 19 ++-- app/src/Talks/Slides/TalkSlides.php | 17 +--- app/tests/Form/InterviewFormFactoryTest.phpt | 67 +++++++++++++ app/tests/Form/TalkFormFactoryTest.phpt | 98 +++++++++++++++++++ app/tests/Media/VideoThumbnailsTest.phpt | 73 ++++++++++++++ app/tests/Media/thumbnail-gif-no-ext | Bin 0 -> 14 bytes app/tests/Media/thumbnail-not-pic | 1 + app/tests/Media/thumbnail-webp-no-ext | Bin 0 -> 26 bytes app/tests/Talks/Slides/TalkSlidesTest.phpt | 14 ++- 14 files changed, 309 insertions(+), 117 deletions(-) create mode 100644 app/tests/Form/InterviewFormFactoryTest.phpt create mode 100644 app/tests/Form/TalkFormFactoryTest.phpt create mode 100644 app/tests/Media/VideoThumbnailsTest.phpt create mode 100644 app/tests/Media/thumbnail-gif-no-ext create mode 100644 app/tests/Media/thumbnail-not-pic create mode 100644 app/tests/Media/thumbnail-webp-no-ext 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 0000000000000000000000000000000000000000..edaf2b97ab8256e904871ce15643d3275dd771e6 GIT binary patch literal 14 TcmZ?wbhEHbWMp7u00L_O6F~vy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f8bf8fa1abcc8705d39a5815d7b0aad344c613f GIT binary patch literal 26 fcmWIYbaNA8U|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 = ?');