diff --git a/lib/Documents/src/DocumentArchiver.php b/lib/Documents/src/DocumentArchiver.php index f89813fc..2ab4782b 100644 --- a/lib/Documents/src/DocumentArchiver.php +++ b/lib/Documents/src/DocumentArchiver.php @@ -24,13 +24,13 @@ public function __construct(FilesystemOperator $documentsStorage) } /** - * @param array $documents + * @param iterable $documents * @param string $name * @param bool $keepFolders * @return string Zip file path * @throws FilesystemException */ - public function archive(array $documents, string $name, bool $keepFolders = true): string + public function archive(iterable $documents, string $name, bool $keepFolders = true): string { $filename = (new AsciiSlugger())->slug($name . ' ' . date('YmdHis'), '_') . '.zip'; $tmpFileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename; @@ -60,8 +60,16 @@ public function archive(array $documents, string $name, bool $keepFolders = true return $tmpFileName; } + /** + * @param iterable $documents + * @param string $name + * @param bool $keepFolders + * @param bool $unlink + * @return BinaryFileResponse + * @throws FilesystemException + */ public function archiveAndServe( - array $documents, + iterable $documents, string $name, bool $keepFolders = true, bool $unlink = true diff --git a/lib/RoadizCoreBundle/src/Api/DataTransformer/TranslationOutputDataTransformer.php b/lib/RoadizCoreBundle/src/Api/DataTransformer/TranslationOutputDataTransformer.php index 41f6d702..e7853fe8 100644 --- a/lib/RoadizCoreBundle/src/Api/DataTransformer/TranslationOutputDataTransformer.php +++ b/lib/RoadizCoreBundle/src/Api/DataTransformer/TranslationOutputDataTransformer.php @@ -7,6 +7,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use RZ\Roadiz\CoreBundle\Api\Dto\TranslationOutput; use RZ\Roadiz\Core\AbstractEntities\TranslationInterface; +use RZ\Roadiz\CoreBundle\Entity\NodesSources; /** * @deprecated Just use `translation_base` serialization group @@ -18,6 +19,9 @@ class TranslationOutputDataTransformer implements DataTransformerInterface */ public function transform($data, string $to, array $context = []): object { + if (!$data instanceof TranslationInterface) { + throw new \InvalidArgumentException('Data to transform must be instance of ' . TranslationInterface::class); + } $output = new TranslationOutput(); $output->locale = $data->getPreferredLocale(); $output->defaultTranslation = $data->isDefaultTranslation(); diff --git a/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php b/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php index b3dab4fe..34cd0091 100644 --- a/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/NodesSourcesRepository.php @@ -147,9 +147,10 @@ protected function filterByCriteria( if ($key == "tags" || $key == "tagExclusive") { continue; } - /* + /** * Main QueryBuilder dispatch loop for * custom properties criteria. + * @var QueryBuilderNodesSourcesBuildEvent $event */ $event = $this->dispatchQueryBuilderBuildEvent($qb, $key, $value); diff --git a/lib/RoadizCoreBundle/src/Repository/PrefixAwareRepository.php b/lib/RoadizCoreBundle/src/Repository/PrefixAwareRepository.php index 52a797df..a21f93ac 100644 --- a/lib/RoadizCoreBundle/src/Repository/PrefixAwareRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/PrefixAwareRepository.php @@ -321,9 +321,11 @@ protected function classicLikeComparison( } foreach ($criteriaFields as $key => $value) { - $realKey = $this->getRealKey($qb, $key); - $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); - $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); + if (\is_string($key)) { + $realKey = $this->getRealKey($qb, $key); + $fullKey = sprintf('LOWER(%s)', $realKey['prefix'] . $realKey['key']); + $qb->orWhere($qb->expr()->like($fullKey, $qb->expr()->literal($value))); + } } return $qb; } diff --git a/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php b/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php index f9728c08..86815976 100644 --- a/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php +++ b/lib/RoadizCoreBundle/src/Repository/UrlAliasRepository.php @@ -4,6 +4,8 @@ namespace RZ\Roadiz\CoreBundle\Repository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\UrlAlias; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -28,18 +30,21 @@ public function __construct( /** * Get all url aliases linked to given node. * - * @param integer $nodeId + * @param int|string|null $nodeId * - * @return array + * @return iterable */ - public function findAllFromNode($nodeId) + public function findAllFromNode(int|string|null $nodeId): iterable { + if (null === $nodeId) { + return []; + } $query = $this->_em->createQuery(' SELECT ua FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua INNER JOIN ua.nodeSource ns INNER JOIN ns.node n WHERE n.id = :nodeId') - ->setParameter('nodeId', (int) $nodeId); + ->setParameter('nodeId', $nodeId); return $query->getResult(); } @@ -48,14 +53,16 @@ public function findAllFromNode($nodeId) * @param string $alias * * @return boolean + * @throws NoResultException + * @throws NonUniqueResultException */ - public function exists($alias) + public function exists(string $alias): bool { $query = $this->_em->createQuery(' SELECT COUNT(ua.alias) FROM RZ\Roadiz\CoreBundle\Entity\UrlAlias ua WHERE ua.alias = :alias') ->setParameter('alias', $alias); - return (bool) $query->getSingleScalarResult(); + return $query->getSingleScalarResult() > 0; } } diff --git a/lib/RoadizCoreBundle/src/Routing/NodePathInfo.php b/lib/RoadizCoreBundle/src/Routing/NodePathInfo.php index 9ee32964..30e8710b 100644 --- a/lib/RoadizCoreBundle/src/Routing/NodePathInfo.php +++ b/lib/RoadizCoreBundle/src/Routing/NodePathInfo.php @@ -96,12 +96,16 @@ public function setContainsScheme(bool $containsScheme): NodePathInfo */ public function serialize(): string { - return \json_encode([ + $json = \json_encode([ 'path' => $this->getPath(), 'parameters' => $this->getParameters(), 'is_complete' => $this->isComplete(), 'contains_scheme' => $this->containsScheme() ]); + if (false === $json) { + throw new \RuntimeException('Unable to serialize NodePathInfo'); + } + return $json; } public function __serialize(): array diff --git a/lib/RoadizCoreBundle/src/Routing/NodeRouteHelper.php b/lib/RoadizCoreBundle/src/Routing/NodeRouteHelper.php index f51117d2..87c96ffe 100644 --- a/lib/RoadizCoreBundle/src/Routing/NodeRouteHelper.php +++ b/lib/RoadizCoreBundle/src/Routing/NodeRouteHelper.php @@ -19,11 +19,11 @@ final class NodeRouteHelper private LoggerInterface $logger; private string $defaultControllerNamespace; /** - * @var class-string + * @var class-string */ private string $defaultControllerClass; /** - * @var class-string|null + * @var class-string|null */ private ?string $controller = null; @@ -54,24 +54,35 @@ public function __construct( /** * Get controller class path for a given node. * - * @return string + * @return class-string|null */ - public function getController(): string + public function getController(): ?string { if (null === $this->controller) { - $namespace = $this->getControllerNamespace(); - $this->controller = $namespace . '\\' . + if (!$this->node->getNodeType()->isReachable()) { + return null; + } + $controllerClassName = $this->getControllerNamespace() . '\\' . StringHandler::classify($this->node->getNodeType()->getName()) . 'Controller'; - /* - * Use a default controller if no controller was found in Theme. - */ - if (!class_exists($this->controller) && $this->node->getNodeType()->isReachable()) { + if (\class_exists($controllerClassName)) { + $reflection = new \ReflectionClass($controllerClassName); + if (!$reflection->isSubclassOf(AbstractController::class)) { + throw new \InvalidArgumentException( + 'Controller class ' . $controllerClassName . ' must extends ' . AbstractController::class + ); + } + // @phpstan-ignore-next-line + $this->controller = $controllerClassName; + } else { + /* + * Use a default controller if no controller was found in Theme. + */ $this->controller = $this->defaultControllerClass; } } - + // @phpstan-ignore-next-line return $this->controller; } @@ -79,8 +90,8 @@ protected function getControllerNamespace(): string { $namespace = $this->defaultControllerNamespace; if (null !== $this->theme) { - $refl = new \ReflectionClass($this->theme->getClassName()); - $namespace = $refl->getNamespaceName() . '\\Controllers'; + $reflection = new \ReflectionClass($this->theme->getClassName()); + $namespace = $reflection->getNamespaceName() . '\\Controllers'; } return $namespace; } diff --git a/lib/RoadizCoreBundle/src/Routing/NodeUrlMatcher.php b/lib/RoadizCoreBundle/src/Routing/NodeUrlMatcher.php index 0ef42348..1e1f7b0a 100644 --- a/lib/RoadizCoreBundle/src/Routing/NodeUrlMatcher.php +++ b/lib/RoadizCoreBundle/src/Routing/NodeUrlMatcher.php @@ -8,6 +8,7 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Theme; use RZ\Roadiz\CoreBundle\Preview\PreviewResolverInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; @@ -21,7 +22,7 @@ final class NodeUrlMatcher extends DynamicUrlMatcher implements NodeUrlMatcherIn { protected PathResolverInterface $pathResolver; /** - * @var class-string + * @var class-string */ private string $defaultControllerClass; @@ -47,7 +48,7 @@ public function getDefaultSupportedFormatExtension(): string * @param PreviewResolverInterface $previewResolver * @param Stopwatch $stopwatch * @param LoggerInterface $logger - * @param class-string $defaultControllerClass + * @param class-string $defaultControllerClass */ public function __construct( PathResolverInterface $pathResolver, diff --git a/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php b/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php index e37cdf9d..94af76a0 100644 --- a/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php +++ b/lib/RoadizCoreBundle/src/Routing/OptimizedNodesSourcesGraphPathAggregator.php @@ -57,7 +57,7 @@ public function aggregatePath(NodesSources $nodesSources, array $parameters = [] /** * @param Node $parent * - * @return array + * @return array */ protected function getParentsIds(Node $parent): array { diff --git a/lib/RoadizCoreBundle/src/SearchEngine/AbstractSearchHandler.php b/lib/RoadizCoreBundle/src/SearchEngine/AbstractSearchHandler.php index eb30fbd0..4b1df8f1 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/AbstractSearchHandler.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/AbstractSearchHandler.php @@ -246,6 +246,9 @@ protected function getFormattedQuery(string $q, int $proximity = 1): array * @see https://lucene.apache.org/solr/guide/6_6/the-standard-query-parser.html#TheStandardQueryParser-FuzzySearches */ $words = preg_split('#[\s,]+#', $q, -1, PREG_SPLIT_NO_EMPTY); + if (false === $words) { + throw new \RuntimeException('Cannot split query string.'); + } $fuzzyiedQuery = implode(' ', array_map(function (string $word) use ($proximity) { /* * Do not fuzz short words: Solr crashes diff --git a/lib/RoadizCoreBundle/src/SearchEngine/ClientRegistry.php b/lib/RoadizCoreBundle/src/SearchEngine/ClientRegistry.php index e15699dc..0e9cae6c 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/ClientRegistry.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/ClientRegistry.php @@ -21,10 +21,17 @@ public function __construct(ContainerInterface $container) public function getClient(): ?Client { - return $this->container->get( + $client = $this->container->get( 'roadiz_core.solr.client', ContainerInterface::NULL_ON_INVALID_REFERENCE ); + if (null === $client) { + return null; + } + if (!($client instanceof Client)) { + throw new \RuntimeException('Solr client must be an instance of ' . Client::class); + } + return $client; } public function isClientReady(?Client $client): bool diff --git a/lib/RoadizCoreBundle/src/SearchEngine/DocumentSearchHandler.php b/lib/RoadizCoreBundle/src/SearchEngine/DocumentSearchHandler.php index 62aec018..2712a21f 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/DocumentSearchHandler.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/DocumentSearchHandler.php @@ -57,9 +57,11 @@ protected function nativeSearch( 'params' => $query->getParams(), ]); - $query = $this->eventDispatcher->dispatch( + /** @var DocumentSearchQueryEvent $event */ + $event = $this->eventDispatcher->dispatch( new DocumentSearchQueryEvent($query, $args) - )->getQuery(); + ); + $query = $event->getQuery(); $solrRequest = $this->getSolr()->execute($query); return $solrRequest->getData(); diff --git a/lib/RoadizCoreBundle/src/SearchEngine/Message/AbstractSolrMessage.php b/lib/RoadizCoreBundle/src/SearchEngine/Message/AbstractSolrMessage.php index b90f762a..58cc2438 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/Message/AbstractSolrMessage.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/Message/AbstractSolrMessage.php @@ -28,7 +28,7 @@ public function __construct(string $classname, mixed $identifier) } /** - * @return string + * @return class-string */ public function getClassname(): string { diff --git a/lib/RoadizCoreBundle/src/SearchEngine/NodeSourceSearchHandler.php b/lib/RoadizCoreBundle/src/SearchEngine/NodeSourceSearchHandler.php index 554b33b5..00d48dcf 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/NodeSourceSearchHandler.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/NodeSourceSearchHandler.php @@ -78,9 +78,11 @@ protected function nativeSearch( 'params' => $query->getParams(), ]); - $query = $this->eventDispatcher->dispatch( + /** @var NodeSourceSearchQueryEvent $event */ + $event = $this->eventDispatcher->dispatch( new NodeSourceSearchQueryEvent($query, $args) - )->getQuery(); + ); + $query = $event->getQuery(); $solrRequest = $this->getSolr()->execute($query); return $solrRequest->getData(); diff --git a/lib/RoadizCoreBundle/src/SearchEngine/SolariumDocumentTranslation.php b/lib/RoadizCoreBundle/src/SearchEngine/SolariumDocumentTranslation.php index 9f0fad0a..be09eed1 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/SolariumDocumentTranslation.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/SolariumDocumentTranslation.php @@ -46,8 +46,9 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new DocumentTranslationIndexingEvent($this->documentTranslation, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var DocumentTranslationIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** diff --git a/lib/RoadizCoreBundle/src/SearchEngine/SolariumNodeSource.php b/lib/RoadizCoreBundle/src/SearchEngine/SolariumNodeSource.php index cf21bdf5..0c9ecf23 100644 --- a/lib/RoadizCoreBundle/src/SearchEngine/SolariumNodeSource.php +++ b/lib/RoadizCoreBundle/src/SearchEngine/SolariumNodeSource.php @@ -51,8 +51,9 @@ public function getDocumentId(): int|string public function getFieldsAssoc(bool $subResource = false): array { $event = new NodesSourcesIndexingEvent($this->nodeSource, [], $this); - - return $this->dispatcher->dispatch($event)->getAssociations(); + /** @var NodesSourcesIndexingEvent $event */ + $event = $this->dispatcher->dispatch($event); + return $event->getAssociations(); } /** diff --git a/lib/RoadizCoreBundle/src/Serializer/Normalizer/TranslationAwareNormalizer.php b/lib/RoadizCoreBundle/src/Serializer/Normalizer/TranslationAwareNormalizer.php index a0441cc9..583afe2f 100644 --- a/lib/RoadizCoreBundle/src/Serializer/Normalizer/TranslationAwareNormalizer.php +++ b/lib/RoadizCoreBundle/src/Serializer/Normalizer/TranslationAwareNormalizer.php @@ -82,13 +82,15 @@ private function getTranslationFromLocale(string $locale): ?TranslationInterface private function getTranslationFromRequest(): ?TranslationInterface { $request = $this->requestStack->getMainRequest(); - if ( - null !== $request && - null !== $translation = $this->getTranslationFromLocale( - $request->query->get('_locale', $request->getLocale()) - ) - ) { - return $translation; + + if (null !== $request) { + $locale = $request->query->get('_locale', $request->getLocale()); + if ( + \is_string($locale) && + null !== $translation = $this->getTranslationFromLocale($locale) + ) { + return $translation; + } } return $this->managerRegistry diff --git a/lib/RoadizCoreBundle/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php b/lib/RoadizCoreBundle/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php index 5e3fc28a..d41adee3 100644 --- a/lib/RoadizCoreBundle/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php +++ b/lib/RoadizCoreBundle/src/Serializer/ObjectConstructor/ChainDoctrineObjectConstructor.php @@ -57,9 +57,11 @@ public function construct( // Locate possible ClassMetadata $classMetadataFactory = $this->entityManager->getMetadataFactory(); + /** @var class-string $className */ + $className = $metadata->name; try { - $doctrineMetadata = $classMetadataFactory->getMetadataFor($metadata->name); - if ($doctrineMetadata->getName() !== $metadata->name) { + $doctrineMetadata = $classMetadataFactory->getMetadataFor($className); + if ($doctrineMetadata->getName() !== $className) { /* * Doctrine resolveTargetEntity has found an alternative class */ @@ -69,7 +71,9 @@ public function construct( // Object class is not a valid doctrine entity } - if ($classMetadataFactory->isTransient($metadata->name)) { + /** @var class-string $className */ + $className = $metadata->name; + if ($classMetadataFactory->isTransient($className)) { // No ClassMetadata found, proceed with normal deserialization return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context); } @@ -77,12 +81,12 @@ public function construct( // Managed entity, check for proxy load if (!\is_array($data)) { // Single identifier, load proxy - return $this->entityManager->getReference($metadata->name, $data); + return $this->entityManager->getReference($className, $data); } /** @var TypedObjectConstructorInterface $typedObjectConstructor */ foreach ($this->typedObjectConstructors as $typedObjectConstructor) { - if ($typedObjectConstructor->supports($metadata->name, $data)) { + if ($typedObjectConstructor->supports($className, $data)) { return $typedObjectConstructor->construct( $visitor, $metadata, @@ -93,10 +97,6 @@ public function construct( } } - // PHPStan need to explicit classname - /** @var class-string $className */ - $className = $metadata->name; - // Fallback to default constructor if missing identifier(s) $classMetadata = $this->entityManager->getClassMetadata($className); $identifierList = []; diff --git a/lib/RoadizCoreBundle/src/Serializer/TranslationAwareContextBuilder.php b/lib/RoadizCoreBundle/src/Serializer/TranslationAwareContextBuilder.php index e12cc612..fb2b9099 100644 --- a/lib/RoadizCoreBundle/src/Serializer/TranslationAwareContextBuilder.php +++ b/lib/RoadizCoreBundle/src/Serializer/TranslationAwareContextBuilder.php @@ -37,15 +37,16 @@ public function createFromRequest(Request $request, bool $normalization, array $ /** @var TranslationRepository $repository */ $repository = $this->managerRegistry ->getRepository(TranslationInterface::class); + $locale = $request->query->get('_locale', $request->getLocale()); + + if (!\is_string($locale)) { + return $context; + } if ($this->previewResolver->isPreview()) { - $translation = $repository->findOneByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); + $translation = $repository->findOneByLocaleOrOverrideLocale($locale); } else { - $translation = $repository->findOneAvailableByLocaleOrOverrideLocale( - $request->query->get('_locale', $request->getLocale()) - ); + $translation = $repository->findOneAvailableByLocaleOrOverrideLocale($locale); } if ($translation instanceof TranslationInterface) { diff --git a/lib/RoadizCoreBundle/src/TwigExtension/NodesSourcesExtension.php b/lib/RoadizCoreBundle/src/TwigExtension/NodesSourcesExtension.php index 59a2b90e..61726ab0 100644 --- a/lib/RoadizCoreBundle/src/TwigExtension/NodesSourcesExtension.php +++ b/lib/RoadizCoreBundle/src/TwigExtension/NodesSourcesExtension.php @@ -76,10 +76,10 @@ public function getTests(): array * @param NodesSources|null $ns * @param array|null $criteria * @param array|null $order - * @return array + * @return iterable * @throws RuntimeError */ - public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null) + public function getChildren(NodesSources $ns = null, array $criteria = null, array $order = null): iterable { if (null === $ns) { if ($this->throwExceptions) { diff --git a/lib/RoadizCoreBundle/src/Xlsx/XlsxExporter.php b/lib/RoadizCoreBundle/src/Xlsx/XlsxExporter.php index 20988aa7..2c24e292 100644 --- a/lib/RoadizCoreBundle/src/Xlsx/XlsxExporter.php +++ b/lib/RoadizCoreBundle/src/Xlsx/XlsxExporter.php @@ -85,7 +85,10 @@ public function exportXlsx($data, $keys = []) foreach ($headerkeys as $key => $value) { $columnAlpha = Coordinate::stringFromColumnIndex($key + 1); $activeSheet->getStyle($columnAlpha . $activeRow)->applyFromArray($headerStyles); - $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $this->translator->trans($value)); + if (\is_string($value)) { + $value = $this->translator->trans($value); + } + $activeSheet->setCellValueByColumnAndRow($key + 1, $activeRow, $value); } $activeRow++; } @@ -134,6 +137,11 @@ public function exportXlsx($data, $keys = []) $writer = new Xlsx($spreadsheet); ob_start(); $writer->save('php://output'); - return ob_get_clean(); + $output = ob_get_clean(); + + if (!\is_string($output)) { + throw new \RuntimeException('Output is not a string.'); + } + return $output; } } diff --git a/lib/RoadizFontBundle/src/Controller/Admin/FontsController.php b/lib/RoadizFontBundle/src/Controller/Admin/FontsController.php index 65b99acc..894ea810 100644 --- a/lib/RoadizFontBundle/src/Controller/Admin/FontsController.php +++ b/lib/RoadizFontBundle/src/Controller/Admin/FontsController.php @@ -154,6 +154,9 @@ public function downloadAction(Request $request, int $id): BinaryFileResponse if ($font !== null) { // Prepare File $file = tempnam(sys_get_temp_dir(), "font_" . $font->getId()); + if (false === $file) { + throw new \RuntimeException('Cannot create temporary file.'); + } $zip = new \ZipArchive(); $zip->open($file, \ZipArchive::CREATE); diff --git a/lib/RoadizFontBundle/src/Controller/FontFaceController.php b/lib/RoadizFontBundle/src/Controller/FontFaceController.php index 502811be..115c0d5f 100644 --- a/lib/RoadizFontBundle/src/Controller/FontFaceController.php +++ b/lib/RoadizFontBundle/src/Controller/FontFaceController.php @@ -86,7 +86,7 @@ public function fontFileAction(Request $request, string $filename, int $variant, if (null !== $font) { [$fontData, $mime] = $this->getFontData($font, $extension); - if (null !== $fontData) { + if (\is_string($fontData)) { $response = new Response( '', Response::HTTP_NOT_MODIFIED, @@ -104,7 +104,7 @@ public function fontFileAction(Request $request, string $filename, int $variant, if (!$response->isNotModified($request)) { $response->setContent($fontData); $response->setStatusCode(Response::HTTP_OK); - $response->setEtag(md5($response->getContent())); + $response->setEtag(md5($fontData)); } return $response; @@ -164,13 +164,12 @@ public function fontFacesAction(Request $request): Response 'variantHash' => $variantHash, ]; } - $response->setContent( - $this->templating->render( - '@RoadizFont/fonts/fontfamily.css.twig', - $assignation - ) + $content = $this->templating->render( + '@RoadizFont/fonts/fontfamily.css.twig', + $assignation ); - $response->setEtag(md5($response->getContent())); + $response->setContent($content); + $response->setEtag(md5($content)); $response->setStatusCode(Response::HTTP_OK); return $response; diff --git a/lib/RoadizRozierBundle/src/Controller/Document/DocumentPublicListController.php b/lib/RoadizRozierBundle/src/Controller/Document/DocumentPublicListController.php index b7ff31de..438330c8 100644 --- a/lib/RoadizRozierBundle/src/Controller/Document/DocumentPublicListController.php +++ b/lib/RoadizRozierBundle/src/Controller/Document/DocumentPublicListController.php @@ -84,20 +84,17 @@ public function indexAction(Request $request, ?int $folderId = null): Response $this->assignation['folder'] = $folder; } - if ( - $request->query->has('type') && - $request->query->get('type', '') !== '' - ) { - $prefilters['mimeType'] = trim($request->query->get('type', '')); - $this->assignation['mimeType'] = trim($request->query->get('type', '')); + $type = $request->query->get('type'); + $embedPlatform = $request->query->get('embedPlatform'); + + if (\is_string($type) && $type !== '') { + $prefilters['mimeType'] = trim($type); + $this->assignation['mimeType'] = trim($type); } - if ( - $request->query->has('embedPlatform') && - $request->query->get('embedPlatform', '') !== '' - ) { - $prefilters['embedPlatform'] = trim($request->query->get('embedPlatform', '')); - $this->assignation['embedPlatform'] = trim($request->query->get('embedPlatform', '')); + if (\is_string($embedPlatform) && $embedPlatform !== '') { + $prefilters['embedPlatform'] = trim($embedPlatform); + $this->assignation['embedPlatform'] = trim($embedPlatform); } /* diff --git a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php index 1b3ceef4..0a8ac1e1 100644 --- a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php +++ b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php @@ -23,12 +23,16 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + $projectDir = $container->getParameter('kernel.project_dir'); + if (!\is_string($projectDir)) { + throw new \RuntimeException('kernel.project_dir parameter is not a string.'); + } $container->setParameter('roadiz_rozier.backoffice_menu_configuration', $config['entries']); $container->setParameter('roadiz_rozier.node_form.class', $config['node_form']); $container->setParameter('roadiz_rozier.add_node_form.class', $config['add_node_form']); $container->setParameter( 'roadiz_rozier.theme_dir', - $container->getParameter('kernel.project_dir') . DIRECTORY_SEPARATOR . trim($config['theme_dir'], "/ \t\n\r\0\x0B") + $projectDir . DIRECTORY_SEPARATOR . trim($config['theme_dir'], "/ \t\n\r\0\x0B") ); $loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__) . '/../config')); diff --git a/lib/RoadizTwoFactorBundle/src/Repository/TwoFactorUserRepository.php b/lib/RoadizTwoFactorBundle/src/Repository/TwoFactorUserRepository.php index 2a6542ed..4f836812 100644 --- a/lib/RoadizTwoFactorBundle/src/Repository/TwoFactorUserRepository.php +++ b/lib/RoadizTwoFactorBundle/src/Repository/TwoFactorUserRepository.php @@ -9,6 +9,9 @@ use RZ\Roadiz\TwoFactorBundle\Entity\TwoFactorUser; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +/** + * @extends EntityRepository + */ class TwoFactorUserRepository extends EntityRepository { public function __construct( diff --git a/lib/RoadizTwoFactorBundle/src/Security/Provider/TwoFactorUserProvider.php b/lib/RoadizTwoFactorBundle/src/Security/Provider/TwoFactorUserProvider.php index 716b76d3..4973c395 100644 --- a/lib/RoadizTwoFactorBundle/src/Security/Provider/TwoFactorUserProvider.php +++ b/lib/RoadizTwoFactorBundle/src/Security/Provider/TwoFactorUserProvider.php @@ -6,10 +6,7 @@ use Doctrine\Persistence\ManagerRegistry; use RZ\Roadiz\CoreBundle\Entity\User; -use RZ\Roadiz\Random\RandomGenerator; -use RZ\Roadiz\Random\TokenGenerator; use RZ\Roadiz\TwoFactorBundle\Entity\TwoFactorUser; -use Scheb\TwoFactorBundle\Model\BackupCodeInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; final class TwoFactorUserProvider implements TwoFactorUserProviderInterface diff --git a/lib/Rozier/src/AjaxControllers/AjaxExplorerProviderController.php b/lib/Rozier/src/AjaxControllers/AjaxExplorerProviderController.php index b9ee2e04..962d2e07 100644 --- a/lib/Rozier/src/AjaxControllers/AjaxExplorerProviderController.php +++ b/lib/Rozier/src/AjaxControllers/AjaxExplorerProviderController.php @@ -4,7 +4,9 @@ namespace Themes\Rozier\AjaxControllers; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Explorer\AbstractExplorerProvider; use RZ\Roadiz\CoreBundle\Explorer\ExplorerItemInterface; use RZ\Roadiz\CoreBundle\Explorer\ExplorerProviderInterface; @@ -26,10 +28,10 @@ public function __construct(ContainerInterface $psrContainer) } /** - * @param class-string $providerClass + * @param class-string $providerClass * @return ExplorerProviderInterface - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \Psr\Container\NotFoundExceptionInterface + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ protected function getProvider(string $providerClass): ExplorerProviderInterface { @@ -38,27 +40,47 @@ protected function getProvider(string $providerClass): ExplorerProviderInterface } return new $providerClass(); } + /** - * @param Request $request - * @return Response JSON response + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function indexAction(Request $request) + protected function getProviderFromRequest(Request $request): ExplorerProviderInterface { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + /** @var class-string|null $providerClass */ + $providerClass = $request->query->get('providerClass'); - if (!$request->query->has('providerClass')) { + if (!\is_string($providerClass)) { throw new InvalidParameterException('providerClass parameter is missing.'); } - - $providerClass = $request->query->get('providerClass'); - if (!class_exists($providerClass)) { + if (!\class_exists($providerClass)) { throw new InvalidParameterException('providerClass is not a valid class.'); } + $reflection = new \ReflectionClass($providerClass); + if (!$reflection->implementsInterface(ExplorerProviderInterface::class)) { + throw new InvalidParameterException('providerClass is not a valid ExplorerProviderInterface class.'); + } + $provider = $this->getProvider($providerClass); if ($provider instanceof AbstractExplorerProvider) { $provider->setContainer($this->psrContainer); } + + return $provider; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function indexAction(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + + $provider = $this->getProviderFromRequest($request); $options = [ 'page' => $request->query->get('page') ?: 1, 'itemPerPage' => $request->query->get('itemPerPage') ?: 30, @@ -99,28 +121,14 @@ public function indexAction(Request $request) * * @param Request $request * @return JsonResponse + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function listAction(Request $request) + public function listAction(Request $request): JsonResponse { - if (!$request->query->has('providerClass')) { - throw new InvalidParameterException('providerClass parameter is missing.'); - } - - $providerClass = $request->query->get('providerClass'); - if (!class_exists($providerClass)) { - throw new InvalidParameterException('providerClass is not a valid class.'); - } - - if (!$request->query->has('ids')) { - throw new InvalidParameterException('Ids should be provided within an array'); - } - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - $provider = $this->getProvider($providerClass); - if ($provider instanceof AbstractExplorerProvider) { - $provider->setContainer($this->psrContainer); - } + $provider = $this->getProviderFromRequest($request); $entitiesArray = []; $cleanNodeIds = array_filter($request->query->filter('ids', [], \FILTER_DEFAULT, [ 'flags' => \FILTER_FORCE_ARRAY diff --git a/lib/Rozier/src/AjaxControllers/AjaxNodesController.php b/lib/Rozier/src/AjaxControllers/AjaxNodesController.php index 0ab98a4e..ea22f6d8 100644 --- a/lib/Rozier/src/AjaxControllers/AjaxNodesController.php +++ b/lib/Rozier/src/AjaxControllers/AjaxNodesController.php @@ -357,7 +357,7 @@ protected function changeNodeStatus(Node $node, string $transition): JsonRespons '%name%' => $node->getNodeName(), '%status%' => $this->getTranslator()->trans(Node::getStatusLabel($node->getStatus())), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); return new JsonResponse( [ diff --git a/lib/Rozier/src/Controllers/AbstractAdminController.php b/lib/Rozier/src/Controllers/AbstractAdminController.php index f27a505a..295a803c 100644 --- a/lib/Rozier/src/Controllers/AbstractAdminController.php +++ b/lib/Rozier/src/Controllers/AbstractAdminController.php @@ -330,7 +330,7 @@ protected function getRequiredDeletionRole(): string } /** - * @return class-string + * @return class-string */ abstract protected function getEntityClass(): string; @@ -398,14 +398,26 @@ protected function getPostSubmitResponse( bool $forceDefaultEditRoute = false, ?Request $request = null ): Response { + if (null === $request) { + // Redirect to default route if no request provided + return $this->redirect($this->urlGenerator->generate( + $this->getEditRouteName(), + $this->getEditRouteParameters($item) + )); + } + + $route = $request->attributes->get('_route'); + $referrer = $request->query->get('referer'); + /* * Force redirect to avoid resending form when refreshing page */ if ( - null !== $request && $request->query->has('referer') && - (new UnicodeString($request->query->get('referer')))->startsWith('/') + \is_string($referrer) && + $referrer !== '' && + (new UnicodeString($referrer))->trim()->startsWith('/') ) { - return $this->redirect($request->query->get('referer')); + return $this->redirect($referrer); } /* @@ -413,8 +425,8 @@ protected function getPostSubmitResponse( */ if ( false === $forceDefaultEditRoute && - null !== $request && - null !== $route = $request->attributes->get('_route') + \is_string($route) && + $route !== '' ) { return $this->redirect($this->urlGenerator->generate( $route, @@ -449,10 +461,11 @@ protected function getPostDeleteResponse(PersistableInterface $item): Response } /** - * @param Event|Event[]|mixed|null $event - * @return object|object[]|null + * @template T of Event|iterable|array|null + * @param T $event + * @return T */ - protected function dispatchSingleOrMultipleEvent($event) + protected function dispatchSingleOrMultipleEvent(mixed $event): mixed { if (null === $event) { return null; @@ -460,10 +473,14 @@ protected function dispatchSingleOrMultipleEvent($event) if ($event instanceof Event) { return $this->dispatchEvent($event); } - if (is_iterable($event)) { + if (\is_iterable($event)) { $events = []; + /** @var Event|null $singleEvent */ foreach ($event as $singleEvent) { - $events[] = $this->dispatchSingleOrMultipleEvent($singleEvent); + $returningEvent = $this->dispatchSingleOrMultipleEvent($singleEvent); + if (null !== $returningEvent) { + $events[] = $returningEvent; + } } return $events; } diff --git a/lib/Rozier/src/Controllers/Attributes/AttributeController.php b/lib/Rozier/src/Controllers/Attributes/AttributeController.php index dcfe7c0d..fd0c0d5b 100644 --- a/lib/Rozier/src/Controllers/Attributes/AttributeController.php +++ b/lib/Rozier/src/Controllers/Attributes/AttributeController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Controllers\AbstractAdminController; +use Twig\Error\RuntimeError; class AttributeController extends AbstractAdminController { @@ -136,8 +137,9 @@ protected function getEntityName(PersistableInterface $item): string /** * @param Request $request * @return Response + * @throws RuntimeError */ - public function importAction(Request $request) + public function importAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES'); @@ -150,6 +152,9 @@ public function importAction(Request $request) if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new \RuntimeException('Cannot read uploaded file.'); + } $this->attributeImporter->import($serializedData); $this->em()->flush(); diff --git a/lib/Rozier/src/Controllers/CustomForms/CustomFormFieldAttributesController.php b/lib/Rozier/src/Controllers/CustomForms/CustomFormFieldAttributesController.php index 3562e29d..0a230ef4 100644 --- a/lib/Rozier/src/Controllers/CustomForms/CustomFormFieldAttributesController.php +++ b/lib/Rozier/src/Controllers/CustomForms/CustomFormFieldAttributesController.php @@ -42,18 +42,18 @@ public function listAction(Request $request, int $customFormAnswerId) } /** - * @param Collection|array $answers + * @param iterable $answers * @return array */ - protected function getAnswersByGroups($answers) + protected function getAnswersByGroups(iterable $answers): array { $fieldsArray = []; /** @var CustomFormFieldAttribute $answer */ foreach ($answers as $answer) { $groupName = $answer->getCustomFormField()->getGroupName(); - if ($groupName != '') { - if (!isset($fieldsArray[$groupName])) { + if (\is_string($groupName) && $groupName !== '') { + if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { $fieldsArray[$groupName] = []; } $fieldsArray[$groupName][] = $answer; diff --git a/lib/Rozier/src/Controllers/Documents/DocumentsController.php b/lib/Rozier/src/Controllers/Documents/DocumentsController.php index ad25bb3a..51abbdd4 100644 --- a/lib/Rozier/src/Controllers/Documents/DocumentsController.php +++ b/lib/Rozier/src/Controllers/Documents/DocumentsController.php @@ -692,6 +692,7 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f /** @var Form $child */ foreach ($form as $child) { if ($child->isSubmitted() && !$child->isValid()) { + /** @var FormError $error */ foreach ($child->getErrors() as $error) { $errorPerForm[$child->getName()][] = $this->getTranslator()->trans($error->getMessage()); } diff --git a/lib/Rozier/src/Controllers/GroupsUtilsController.php b/lib/Rozier/src/Controllers/GroupsUtilsController.php index f15b6499..e090be63 100644 --- a/lib/Rozier/src/Controllers/GroupsUtilsController.php +++ b/lib/Rozier/src/Controllers/GroupsUtilsController.php @@ -120,6 +120,9 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new RuntimeError('Cannot read uploaded file.'); + } if (null !== \json_decode($serializedData)) { $this->groupsImporter->import($serializedData); diff --git a/lib/Rozier/src/Controllers/Nodes/NodesController.php b/lib/Rozier/src/Controllers/Nodes/NodesController.php index 220f00ff..cc850ff9 100644 --- a/lib/Rozier/src/Controllers/Nodes/NodesController.php +++ b/lib/Rozier/src/Controllers/Nodes/NodesController.php @@ -546,11 +546,12 @@ public function deleteAction(Request $request, int $nodeId): Response ); $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); + $referrer = $request->query->get('referer'); if ( - $request->query->has('referer') && - (new UnicodeString($request->query->get('referer')))->startsWith('/') + \is_string($referrer) && + (new UnicodeString($referrer))->trim()->startsWith('/') ) { - return $this->redirect($request->query->get('referer')); + return $this->redirect($referrer); } if (null !== $parent) { return $this->redirectToRoute( @@ -752,7 +753,9 @@ public function publishAllAction(Request $request, int $nodeId): Response return $this->redirectToRoute('nodesEditSourcePage', [ 'nodeId' => $nodeId, - 'translationId' => $node->getNodeSources()->first()->getTranslation()->getId(), + 'translationId' => $node->getNodeSources()->first() ? + $node->getNodeSources()->first()->getTranslation()->getId() : + null, ]); } diff --git a/lib/Rozier/src/Controllers/Nodes/NodesUtilsController.php b/lib/Rozier/src/Controllers/Nodes/NodesUtilsController.php index a25fa9bd..d59a2acf 100644 --- a/lib/Rozier/src/Controllers/Nodes/NodesUtilsController.php +++ b/lib/Rozier/src/Controllers/Nodes/NodesUtilsController.php @@ -61,7 +61,7 @@ public function duplicateAction(Request $request, int $nodeId) '%name%' => $existingNode->getNodeName(), ]); - $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first() ?: null); return $this->redirectToRoute( 'nodesEditPage', diff --git a/src/Controller/PageController.php b/src/Controller/PageController.php new file mode 100644 index 00000000..ce70bdd7 --- /dev/null +++ b/src/Controller/PageController.php @@ -0,0 +1,19 @@ +render('nodeSource/page.html.twig', [ + 'nodeSource' => $nodeSource + ]); + } +} diff --git a/src/GeneratedEntity/NSPage.php b/src/GeneratedEntity/NSPage.php index 87c5a19d..bd692ef8 100644 --- a/src/GeneratedEntity/NSPage.php +++ b/src/GeneratedEntity/NSPage.php @@ -644,58 +644,6 @@ public function addCustomForm(\RZ\Roadiz\CoreBundle\Entity\CustomForm $customFor } - /** - * Main user. - * Default values: # Entity class name - * classname: \RZ\Roadiz\CoreBundle\Entity\User - * # Displayable is the method used to display entity name - * displayable: getUsername - * # Same as Displayable but for a secondary information - * alt_displayable: getEmail - * # Same as Displayable but for a secondary information - * thumbnail: ~ - * # Searchable entity fields - * searchable: - * - username - * - email - * # This order will only be used for explorer - * orderBy: - * - field: email - * direction: ASC - * @var \RZ\Roadiz\CoreBundle\Entity\User|null - */ - #[ - SymfonySerializer\SerializedName(serializedName: "mainUser"), - SymfonySerializer\Groups(["nodes_sources", "nodes_sources_default"]), - SymfonySerializer\MaxDepth(2), - ORM\ManyToOne(targetEntity: \RZ\Roadiz\CoreBundle\Entity\User::class), - ORM\JoinColumn(name: "main_user_id", referencedColumnName: "id", onDelete: "SET NULL"), - ApiFilter(OrmFilter\SearchFilter::class, strategy: "exact"), - Serializer\Groups(["nodes_sources", "nodes_sources_default"]), - Serializer\MaxDepth(2) - ] - private ?\RZ\Roadiz\CoreBundle\Entity\User $mainUser = null; - - /** - * @return \RZ\Roadiz\CoreBundle\Entity\User|null - */ - public function getMainUser(): ?\RZ\Roadiz\CoreBundle\Entity\User - { - return $this->mainUser; - } - - /** - * @param \RZ\Roadiz\CoreBundle\Entity\User|null $mainUser - * @return $this - */ - public function setMainUser(?\RZ\Roadiz\CoreBundle\Entity\User $mainUser = null): static - { - $this->mainUser = $mainUser; - - return $this; - } - - /** * Reference to users * @@ -1165,6 +1113,58 @@ public function setLayout(?string $layout): static } + /** + * Main user. + * Default values: # Entity class name + * classname: \RZ\Roadiz\CoreBundle\Entity\User + * # Displayable is the method used to display entity name + * displayable: getUsername + * # Same as Displayable but for a secondary information + * alt_displayable: getEmail + * # Same as Displayable but for a secondary information + * thumbnail: ~ + * # Searchable entity fields + * searchable: + * - username + * - email + * # This order will only be used for explorer + * orderBy: + * - field: email + * direction: ASC + * @var \RZ\Roadiz\CoreBundle\Entity\User|null + */ + #[ + SymfonySerializer\SerializedName(serializedName: "mainUser"), + SymfonySerializer\Groups(["nodes_sources", "nodes_sources_default"]), + SymfonySerializer\MaxDepth(2), + ORM\ManyToOne(targetEntity: \RZ\Roadiz\CoreBundle\Entity\User::class), + ORM\JoinColumn(name: "main_user_id", referencedColumnName: "id", onDelete: "SET NULL"), + ApiFilter(OrmFilter\SearchFilter::class, strategy: "exact"), + Serializer\Groups(["nodes_sources", "nodes_sources_default"]), + Serializer\MaxDepth(2) + ] + private ?\RZ\Roadiz\CoreBundle\Entity\User $mainUser = null; + + /** + * @return \RZ\Roadiz\CoreBundle\Entity\User|null + */ + public function getMainUser(): ?\RZ\Roadiz\CoreBundle\Entity\User + { + return $this->mainUser; + } + + /** + * @param \RZ\Roadiz\CoreBundle\Entity\User|null $mainUser + * @return $this + */ + public function setMainUser(?\RZ\Roadiz\CoreBundle\Entity\User $mainUser = null): static + { + $this->mainUser = $mainUser; + + return $this; + } + + public function __construct(\RZ\Roadiz\CoreBundle\Entity\Node $node, \RZ\Roadiz\CoreBundle\Entity\Translation $translation) { parent::__construct($node, $translation); diff --git a/templates/nodeSource/page.html.twig b/templates/nodeSource/page.html.twig new file mode 100644 index 00000000..4fb0c2b8 --- /dev/null +++ b/templates/nodeSource/page.html.twig @@ -0,0 +1,8 @@ +{% extends "@RoadizCore/base.html.twig" %} + +{% block content %} +

Page: {{ nodeSource.title }}

+ {% if nodeSource.content is defined %} + {{ nodeSource.content|markdown }} + {% endif %} +{% endblock %}