diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 8347238cc..f0b5e171f 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -40,7 +40,7 @@ web_environment: - PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage --ignore-certificate-errors' - PANTHER_NO_SANDBOX=1 - PANTHER_NO_HEADLESS=0 -nodejs_version: "16" +nodejs_version: "18" webimage_extra_packages: - pngquant - jpegoptim diff --git a/.github/workflows/main-ci.yaml b/.github/workflows/main-ci.yaml index 84033692c..229b90477 100644 --- a/.github/workflows/main-ci.yaml +++ b/.github/workflows/main-ci.yaml @@ -43,6 +43,9 @@ jobs: strategy: matrix: include: + - ibexa_version: 4.6.7 + php: 8.1 + node: 18.x - ibexa_version: 4.5.* php: 8.1 node: 14.x diff --git a/bin/ci-should b/bin/ci-should index e665670dd..0647e9af5 100755 --- a/bin/ci-should +++ b/bin/ci-should @@ -43,7 +43,7 @@ $requiredIbexaVersion = $config['required_ibexa_version'] ?? null; if ( $requiredIbexaVersion && $ibexaVersion ) { if(!Semver::satisfies( $ibexaVersion, $requiredIbexaVersion )) { - exit( 0 ); + exit( 1 ); } } exit( ( $config[$action] ?? false ) === true ? 0 : 1 ); diff --git a/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.en.yml b/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.en.yml new file mode 100644 index 000000000..8bd1e7752 --- /dev/null +++ b/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.en.yml @@ -0,0 +1 @@ +menu_manager: Menu manager \ No newline at end of file diff --git a/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml b/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml new file mode 100644 index 000000000..0f5f4b9d4 --- /dev/null +++ b/components/MenuManagerBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml @@ -0,0 +1 @@ +menu_manager: Gestion des menus \ No newline at end of file diff --git a/components/SEOBundle/bundle/Controller/Admin/RedirectController.php b/components/SEOBundle/bundle/Controller/Admin/RedirectController.php index 65ab76e49..adcf08e01 100644 --- a/components/SEOBundle/bundle/Controller/Admin/RedirectController.php +++ b/components/SEOBundle/bundle/Controller/Admin/RedirectController.php @@ -80,7 +80,7 @@ public function listAction( $urlExists = $urlWildcardService->translate($destination); $errors[] = $translator->trans('nova.redirect.create.exists', ['url' => $destination], 'redirect'); } catch (Exception $e) { - $e->getMessage(); + $errors[] = $e->getMessage(); } if (('' !== $source || '' !== $destination) && ($source !== $destination) && (null === $urlExists)) { @@ -109,7 +109,7 @@ public function listAction( } $page = $request->query->get('page') ?? 1; - $pagerfanta = new Pagerfanta(new ArrayAdapter($urlWildcardService->loadAll())); + $pagerfanta = new Pagerfanta(new ArrayAdapter((array) $urlWildcardService->loadAll())); $pagerfanta->setMaxPerPage(self::URL_LIMIT); $pagerfanta->setCurrentPage(min($page, $pagerfanta->getNbPages())); diff --git a/components/SEOBundle/bundle/Controller/SitemapController.php b/components/SEOBundle/bundle/Controller/SitemapController.php index a27581e85..c71857c87 100644 --- a/components/SEOBundle/bundle/Controller/SitemapController.php +++ b/components/SEOBundle/bundle/Controller/SitemapController.php @@ -16,16 +16,19 @@ use DOMDocument; use DOMElement; use Ibexa\Bundle\Core\Controller; +use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Values\Content\Location; -use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; use Ibexa\Contracts\Core\Variation\VariationHandler; use Ibexa\Core\Helper\FieldHelper; use Ibexa\Core\MVC\Symfony\Routing\UrlAliasRouter; +use Novactive\Bundle\eZSEOBundle\Core\Helper\SiteMapHelper; use Novactive\Bundle\eZSEOBundle\Core\Sitemap\QueryFactory; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\RouterInterface; +use Throwable; class SitemapController extends Controller { @@ -35,6 +38,15 @@ class SitemapController extends Controller /** @var VariationHandler */ protected $imageVariationService; + /** @var RouterInterface */ + protected $router; + + /** @var SiteMapHelper */ + protected $siteMapHelper; + + /** @var Repository */ + private $repository; + /** * How many in a Sitemap. * @@ -42,10 +54,18 @@ class SitemapController extends Controller */ public const PACKET_MAX = 1000; - public function __construct(FieldHelper $fieldHelper, VariationHandler $imageVariationService) - { + public function __construct( + FieldHelper $fieldHelper, + VariationHandler $imageVariationService, + RouterInterface $router, + SiteMapHelper $siteMapHelper, + Repository $repository + ) { $this->fieldHelper = $fieldHelper; $this->imageVariationService = $imageVariationService; + $this->router = $router; + $this->siteMapHelper = $siteMapHelper; + $this->repository = $repository; } /** @@ -53,33 +73,41 @@ public function __construct(FieldHelper $fieldHelper, VariationHandler $imageVar */ public function indexAction(QueryFactory $queryFactory): Response { - $searchService = $this->getRepository()->getSearchService(); - $query = $queryFactory(); - $query->limit = 0; - $resultCount = $searchService->findLocations($query)->totalCount; - + $isMultisiteAccess = $this->getConfigResolver() + ->getParameter('multi_siteaccess_sitemap', 'nova_ezseo') ?? false; // Dom Doc $sitemap = new DOMDocument('1.0', 'UTF-8'); $sitemap->formatOutput = true; - - // create an index if we are greater than th PACKET_MAX - if ($resultCount > static::PACKET_MAX) { + // Create an index for multi site + if ($isMultisiteAccess) { $root = $sitemap->createElement('sitemapindex'); $root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); $sitemap->appendChild($root); - $this->fillSitemapIndex($sitemap, $resultCount, $root); + $this->fillSitemapMultiSiteIndex($sitemap, $root, $queryFactory); } else { - // if we are less or equal than the PACKET_SIZE, redo the search with no limit and list directly the urlmap - $query->limit = $resultCount; - $results = $searchService->findLocations($query); - $root = $sitemap->createElement('urlset'); - $root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); - $root->setAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1'); - $this->fillSitemap($sitemap, $root, $results); - $sitemap->appendChild($root); - } + $searchService = $this->getRepository()->getSearchService(); + $query = $queryFactory(); + $query->limit = 0; + $resultCount = $searchService->findLocations($query)->totalCount; + // create an index if we are greater than th PACKET_MAX + if ($resultCount > static::PACKET_MAX) { + $root = $sitemap->createElement('sitemapindex'); + $root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + $sitemap->appendChild($root); + $this->fillSitemapIndex($sitemap, $resultCount, $root); + } else { + // if we are less or equal than the PACKET_SIZE, redo the search with no limit and list directly the urlmap + $query->limit = $resultCount; + $results = $searchService->findLocations($query); + $root = $sitemap->createElement('urlset'); + $root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + $root->setAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1'); + $this->fillSitemap($sitemap, $root, $results); + $sitemap->appendChild($root); + } + } $response = new Response($sitemap->saveXML(), 200, ['Content-type' => 'text/xml']); $response->setSharedMaxAge(86400); @@ -93,19 +121,27 @@ public function indexAction(QueryFactory $queryFactory): Response */ public function pageAction(QueryFactory $queryFactory, int $page = 1): Response { + $isMultisiteAccess = $this->getConfigResolver() + ->getParameter('multi_siteaccess_sitemap', 'nova_ezseo') ?? false; + $sitemap = new DOMDocument('1.0', 'UTF-8'); $root = $sitemap->createElement('urlset'); $sitemap->formatOutput = true; $root->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); $root->setAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1'); - $sitemap->appendChild($root); - $query = $queryFactory(); - $query->limit = static::PACKET_MAX; - $query->offset = static::PACKET_MAX * ($page - 1); - $searchService = $this->getRepository()->getSearchService(); - $results = $searchService->findLocations($query); - $this->fillSitemap($sitemap, $root, $results); + // Create an index for multi site + if ($isMultisiteAccess) { + $this->multisiteAccessPage($sitemap, $root, $queryFactory, $page); + } else { + $sitemap->appendChild($root); + $query = $queryFactory(); + $query->limit = static::PACKET_MAX; + $query->offset = static::PACKET_MAX * ($page - 1); + $searchService = $this->getRepository()->getSearchService(); + $results = $searchService->findLocations($query); + $this->fillSitemap($sitemap, $root, $results); + } $response = new Response($sitemap->saveXML(), 200, ['Content-type' => 'text/xml']); $response->setSharedMaxAge(86400); @@ -113,6 +149,41 @@ public function pageAction(QueryFactory $queryFactory, int $page = 1): Response return $response; } + public function multisiteAccessPage( + DOMDocument $sitemap, + DOMElement $root, + QueryFactory $queryFactory, + int $page = 1 + ): void { + $schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9" . + " http://www.w3.org/1999/xhtml http://www.w3.org/2002/08/xhtml/xhtml1-strict.xsd". + " http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd http://www.google.com/schemas/sitemap-image/1.1". + " http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" . + " http://www.google.com/schemas/sitemap-video/1.1"; + $root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + $root->setAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + $root->setAttribute('xmlns:video', 'http://www.google.com/schemas/sitemap-video/1.1'); + $root->setAttribute('xsi:schemaLocation', $schemaLocation); + $sitemap->appendChild($root); + //Get The site Access of selected Host and local + $currentSiteAccess = $this->siteMapHelper->getCurrentSiteAccess(); + if ($currentSiteAccess) { + $rootLocationId = $this->siteMapHelper->getCurrentSiteAccessRootLocationId(); + $mainLanguage = $this->siteMapHelper->getCurrentSiteAccessMainLanguage(); + $query = $queryFactory( + $rootLocationId, + [$mainLanguage], + false + ); + $query->limit = static::PACKET_MAX; + $query->offset = static::PACKET_MAX * ($page - 1); + $searchService = $this->getRepository()->getSearchService(); + + $results = $searchService->findLocations($query); + $this->fillMultiLanguagesSitemap($sitemap, $root, $results); + } + } + /** * Fill a sitemap. */ @@ -120,7 +191,6 @@ protected function fillSitemap(DOMDocument $sitemap, DOMElement $root, SearchRes { foreach ($results->searchHits as $searchHit) { /** - * @var SearchHit * @var Location $location */ $location = $searchHit->valueObject; @@ -131,9 +201,7 @@ protected function fillSitemap(DOMDocument $sitemap, DOMElement $root, SearchRes UrlGeneratorInterface::ABSOLUTE_URL ); } catch (\Exception $exception) { - if ($this->has('logger')) { - $this->get('logger')->error('NovaeZSEO: '.$exception->getMessage()); - } + $this->siteMapHelper->logException($exception); continue; } @@ -146,36 +214,8 @@ protected function fillSitemap(DOMDocument $sitemap, DOMElement $root, SearchRes $lastmod = $sitemap->createElement('lastmod', $modified); $urlElt = $sitemap->createElement('url'); - // Inject the image tags if config is enabl - - $displayImage = $this->getConfigResolver()->getParameter('display_images_in_sitemap', 'nova_ezseo'); - if (true === $displayImage) { - $content = $this->getRepository()->getContentService()->loadContentByContentInfo( - $location->contentInfo - ); - foreach ($content->getFields() as $field) { - $fieldTypeIdentifier = $content->getContentType()->getFieldDefinition( - $field->fieldDefIdentifier - )->fieldTypeIdentifier; - - if ('ezimage' !== $fieldTypeIdentifier) { - continue; - } - - if ($this->fieldHelper->isFieldEmpty($content, $field->fieldDefIdentifier)) { - continue; - } - $variation = $this->imageVariationService->getVariation( - $field, - $content->getVersionInfo(), - 'original' - ); - $imageContainer = $sitemap->createElement('image:image'); - $imageLoc = $sitemap->createElement('image:loc', $variation->uri); - $imageContainer->appendChild($imageLoc); - $urlElt->appendChild($imageContainer); - } - } + // Inject the image tags if config is enabled + $this->injectImageTag($location, $sitemap, $urlElt); $urlElt->appendChild($loc); $urlElt->appendChild($lastmod); @@ -199,9 +239,7 @@ protected function fillSitemapIndex(DOMDocument $sitemap, int $numberOfResults, UrlGeneratorInterface::ABSOLUTE_URL ); } catch (\Exception $exception) { - if ($this->has('logger')) { - $this->get('logger')->error('NovaeZSEO: '.$exception->getMessage()); - } + $this->siteMapHelper->logException($exception); continue; } @@ -214,4 +252,161 @@ protected function fillSitemapIndex(DOMDocument $sitemap, int $numberOfResults, $root->appendChild($sitemapElt); } } + protected function fillSitemapMultiSiteIndex( + DOMDocument $sitemap, + DOMElement $root, + QueryFactory $queryFactory + ): void { + $siteMapHelper = $this->siteMapHelper; + $this->repository->sudo(static function (Repository $repository) use ( + $sitemap, + $root, + $queryFactory, + $siteMapHelper + ) { + $siteAccesses = $siteMapHelper->getSiteAccessesLocationIdLanguages(); + foreach ($siteAccesses as $siteAccess => $rootLocationLanguages) { + $rootLocationId = $rootLocationLanguages['rootLocationId']; + $query = $queryFactory( + $rootLocationId, + [$rootLocationLanguages['mainLanguage']], + false + ); + $query->limit = 0; + $numberOfResults = $repository->getSearchService()->findLocations($query)->totalCount; + $numberOfPage = (int) ceil($numberOfResults / static::PACKET_MAX); + for ($sitemapNumber = 1; $sitemapNumber <= $numberOfPage; ++$sitemapNumber) { + $sitemapElt = $sitemap->createElement('sitemap'); + $locUrl = $siteMapHelper->generateRouteUrl( + '_novaseo_sitemap_page', + $siteAccess, + ['page' => $sitemapNumber] + ); + $loc = $sitemap->createElement('loc', $locUrl); + $date = new DateTime(); + $modificationDate = $date->format('c'); + $mod = $sitemap->createElement('lastmod', $modificationDate); + $sitemapElt->appendChild($loc); + $sitemapElt->appendChild($mod); + $root->appendChild($sitemapElt); + } + } + }); + } + + /** + * Fill a sitemap. + */ + protected function fillMultiLanguagesSitemap( + DOMDocument $sitemap, + DOMElement $root, + SearchResult $results + ): void { + $currentSiteAccess = $this->siteMapHelper->getCurrentSiteAccess(); + foreach ($results->searchHits as $searchHit) { + /** + * @var Location $location + */ + $location = $searchHit->valueObject; + + $mainLanguageUrl = $this->siteMapHelper->generateLocationUrl($location->id, $currentSiteAccess); + if (null === $mainLanguageUrl || 0 != strpos($mainLanguageUrl, 'view/content/')) { + continue; + } + + $modified = $location->contentInfo->modificationDate->format('c'); + $loc = $sitemap->createElement('loc', $mainLanguageUrl); + $lastmod = $sitemap->createElement('lastmod', $modified); + $urlElt = $sitemap->createElement('url'); + + $urlElt->appendChild($loc); + // Inject the image tags if config is enabled + $this->injectImageTag($location, $sitemap, $urlElt); + // Inject the alternate lang tags if config is enabled + $this->injectAlternateLangTag($location, $sitemap, $urlElt); + + $urlElt->appendChild($lastmod); + $root->appendChild($urlElt); + } + } + + public function injectAlternateLangTag($location, DOMDocument $sitemap, DOMElement $root): void + { + $isMultiLanguages = $this->getConfigResolver()->getParameter('multi_languages_sitemap', 'nova_ezseo'); + if ($isMultiLanguages) { + try { + $siteAccesses = $this->getConfigResolver()->getParameter('translation_siteaccesses'); + $languagesCodes = []; + $contentInfo = $location->contentInfo; + $contentLanguages = $this->repository->getContentService() + ->loadVersionInfo($contentInfo)->getLanguages(); + foreach ($contentLanguages as $language) { + $languagesCodes[] = $language->languageCode; + } + foreach ($siteAccesses as $siteAccess) { + $siteAccessMainLanguage = $this->siteMapHelper->getSiteAccessMainLanguage($siteAccess); + if (!in_array($siteAccessMainLanguage, $languagesCodes)) { + continue; + } + + $url = $this->siteMapHelper->generateLocationUrl($location->id, $siteAccess); + $hreflang = $this->siteMapHelper->getHrefLang($siteAccessMainLanguage); + if (null === $url || 0 != strpos($url, 'view/content/')) { + continue; + } + + $xhtml = $sitemap->createElement('xhtml:link'); + $xhtml->setAttribute('rel', 'alternate'); + $xhtml->setAttribute('hreflang', $hreflang); + $xhtml->setAttribute('href', $url); + $root->appendChild($xhtml); + } + } catch (Throwable $e) { + $this->siteMapHelper->logException($e); + } + } + } + + public function injectImageTag($location, DOMDocument $sitemap, DOMElement $root): void + { + $displayImage = $this->getConfigResolver()->getParameter('display_images_in_sitemap', 'nova_ezseo'); + + if (true === $displayImage) { + try { + $content = $this->getRepository()->getContentService()->loadContentByContentInfo( + $location->contentInfo + ); + } catch (Throwable $exception) { + return; + } + foreach ($content->getFields() as $field) { + $fieldTypeIdentifier = $content->getContentType()->getFieldDefinition( + $field->fieldDefIdentifier + )->fieldTypeIdentifier; + + if ('ezimage' !== $fieldTypeIdentifier && 'ezimageasset' !== $fieldTypeIdentifier) { + continue; + } + + if ($this->fieldHelper->isFieldEmpty($content, $field->fieldDefIdentifier)) { + continue; + } + try { + $variation = $this->imageVariationService->getVariation( + $field, + $content->getVersionInfo(), + 'original' + ); + + $imageContainer = $sitemap->createElement('image:image'); + $imageLoc = $sitemap->createElement('image:loc', $variation->uri); + $imageContainer->appendChild($imageLoc); + $root->appendChild($imageContainer); + } catch (Throwable $exception) { + $this->siteMapHelper->logException($exception); + continue; + } + } + } + } } diff --git a/components/SEOBundle/bundle/Core/FieldType/Metas/FormMapper.php b/components/SEOBundle/bundle/Core/FieldType/Metas/FormMapper.php index a29420dfb..090e4af67 100644 --- a/components/SEOBundle/bundle/Core/FieldType/Metas/FormMapper.php +++ b/components/SEOBundle/bundle/Core/FieldType/Metas/FormMapper.php @@ -89,7 +89,7 @@ public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data) $metasData = $data->value->metas; foreach ($metasConfig as $key => $meta) { $content = isset($metasData[$key]) ? $metasData[$key]->getContent() : null; - $fieldType = $meta['type']; + $fieldType = $meta['type'] ?? 'text'; if (isset($metasData[$key]) && '' != $metasData[$key]->getFieldType()) { $fieldType = $metasData[$key]->getFieldType(); } diff --git a/components/SEOBundle/bundle/Core/FieldType/Metas/Type.php b/components/SEOBundle/bundle/Core/FieldType/Metas/Type.php index ba334ff69..c0d938d77 100644 --- a/components/SEOBundle/bundle/Core/FieldType/Metas/Type.php +++ b/components/SEOBundle/bundle/Core/FieldType/Metas/Type.php @@ -144,7 +144,7 @@ public function getName(SPIValue $value, FieldDefinition $fieldDefinition, strin /** * Returns information for FieldValue->$sortKey relevant to the field type. */ - protected function getSortInfo(CoreValue $value): bool + protected function getSortInfo(SPIValue $value): bool { return false; } diff --git a/components/SEOBundle/bundle/Core/Helper/SiteMapHelper.php b/components/SEOBundle/bundle/Core/Helper/SiteMapHelper.php new file mode 100644 index 000000000..89443f0d8 --- /dev/null +++ b/components/SEOBundle/bundle/Core/Helper/SiteMapHelper.php @@ -0,0 +1,164 @@ +configResolver = $configResolver; + $this->siteAccessService = $siteAccessService; + $this->routeReferenceGenerator = $routeReferenceGenerator; + $this->router = $router; + $this->localeConverter = $localeConverter; + $this->logger = $logger ?? new NullLogger(); + } + + public function generateLocationUrl( + int $locationId, + string $siteAccess = null + ): ?string { + try { + $routeParams['locationId'] = $locationId; + $routeReference = $this->routeReferenceGenerator->generate( + UrlAliasRouter::URL_ALIAS_ROUTE_NAME, + $routeParams + ); + if ($siteAccess) { + $routeReference->set('siteaccess', $siteAccess); + } + $url = $this->router->generate( + UrlAliasRouter::URL_ALIAS_ROUTE_NAME, + $routeReference->getParams(), + UrlGeneratorInterface::ABSOLUTE_URL + ); + } catch (Throwable $exception) { + $this->logger->error('NovaeZSEO: ' . $exception->getMessage()); + $url = null; + } + + return $url; + } + public function generateRouteUrl( + string $routeName, + string $siteAccess = null, + array $parameters = [] + ): ?string { + try { + $url = $this->router->generate( + $routeName, + [...['siteaccess' => $siteAccess], ...$parameters], + UrlGeneratorInterface::ABSOLUTE_URL + ); + } catch (Throwable $exception) { + $this->logger->error('NovaeZSEO: ' . $exception->getMessage()); + $url = null; + } + + return $url; + } + public function getSiteAccessesLocationIdLanguages(): array + { + $rootLocationLanguages = []; + $siteAccesses = $this->configResolver->getParameter('translation_siteaccesses'); + foreach ($siteAccesses as $siteAccess) { + $rootLocationLanguages[$siteAccess] = [ + 'rootLocationId' => $this->getSiteAccessRootLocationId($siteAccess), + 'mainLanguage' => $this->getSiteAccessMainLanguage($siteAccess), + 'languages' => $this->getSiteAccessLanguages($siteAccess) + ]; + } + + return $rootLocationLanguages; + } + + public function getCurrentSiteAccess(): ?string + { + return $this->siteAccessService->getCurrent()?->name; + } + + public function getCurrentSiteAccessRootLocationId(): ?int + { + return $this->getSiteAccessRootLocationId($this->getCurrentSiteAccess()); + } + public function getCurrentSiteAccessMainLanguage(): ?string + { + return $this->getSiteAccessMainLanguage($this->getCurrentSiteAccess()); + } + + public function getSiteAccessRootLocationId(string $siteAccess): ?int + { + return $this->configResolver->getParameter('content.tree_root.location_id', null, $siteAccess); + } + + public function getSiteAccessLanguages(string $siteAccess): array + { + return (array) $this->configResolver->getParameter('languages', null, $siteAccess); + } + + public function getSiteAccessMainLanguage(string $siteAccess): string + { + $languages = $this->configResolver->getParameter('languages', null, $siteAccess); + return array_shift($languages); + } + + public function getHrefLang(string $languageCode): string + { + return str_replace( + '_', + '-', + ($this->localeConverter->convertToPOSIX($languageCode) ?? '') + ); + } + + public function logException(Exception $exception): void + { + $this->logger?->error('NovaeZSEO: ' . $exception->getMessage()); + } +} diff --git a/components/SEOBundle/bundle/Core/MetaNameSchema.php b/components/SEOBundle/bundle/Core/MetaNameSchema.php index 5d07844f2..eaf44af1e 100644 --- a/components/SEOBundle/bundle/Core/MetaNameSchema.php +++ b/components/SEOBundle/bundle/Core/MetaNameSchema.php @@ -36,6 +36,7 @@ use Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper; use Ibexa\Core\Repository\Values\Content\VersionInfo; use Ibexa\FieldTypeRichText\FieldType\RichText\Value as RichTextValue; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class MetaNameSchema extends NameSchemaService { @@ -77,20 +78,22 @@ class MetaNameSchema extends NameSchemaService public function __construct( ContentTypeHandler $contentTypeHandler, FieldTypeRegistry $fieldTypeRegistry, + EventDispatcherInterface $eventDispatcher, ContentLanguageHandler $languageHandler, RepositoryInterface $repository, TranslationHelper $translationHelper, ConfigResolverInterface $configurationResolver, array $settings = [] ) { + $this->fieldTypeRegistry = $fieldTypeRegistry; $settings['limit'] = $this->fieldContentMaxLength; $handler = new ContentTypeDomainMapper( $contentTypeHandler, $languageHandler, - $this->fieldTypeRegistry + $fieldTypeRegistry ); - parent::__construct($contentTypeHandler, $handler, $fieldTypeRegistry, $settings); + parent::__construct($contentTypeHandler, $handler, $fieldTypeRegistry, $eventDispatcher, $settings); $this->repository = $repository; $this->translationHelper = $translationHelper; @@ -113,7 +116,7 @@ public function resolveMeta(Meta $meta, Content $content, ContentType $contentTy { $languages = $this->configurationResolver->getParameter('languages'); - $resolveMultilingue = $this->resolve( + $resolveMultilingue = $this->resolveNameSchema( $meta->getContent(), $content->getContentType(), $content->fields, @@ -329,4 +332,15 @@ protected function handleImageAssetValue(ImageAssetValue $value, $fieldDefinitio return ''; } + + /** + * Override native function as this prevent usage of `()` inside metas in Ibexa 4.6 + * {@inheritDoc} + */ + protected function filterNameSchema(string $nameSchema): array + { + $groupLookupTable = []; + + return [$nameSchema, $groupLookupTable]; + } } diff --git a/components/SEOBundle/bundle/Core/Sitemap/QueryFactory.php b/components/SEOBundle/bundle/Core/Sitemap/QueryFactory.php index b2bc4e338..d0d7405ca 100644 --- a/components/SEOBundle/bundle/Core/Sitemap/QueryFactory.php +++ b/components/SEOBundle/bundle/Core/Sitemap/QueryFactory.php @@ -6,6 +6,7 @@ use Ibexa\Bundle\Core\DependencyInjection\Configuration\ConfigResolver; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException; +use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException; use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Values\Content\Location; use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery as Query; @@ -52,8 +53,11 @@ private function getRootLocation(): Location ); } - public function __invoke(): Query - { + public function __invoke( + int $rootLocationId = null, + array $languages = [], + bool $matchAlwaysAvailable = true + ): Query { $query = new Query(); // always here, we want visible Contents @@ -61,7 +65,16 @@ public function __invoke(): Query // do we want to limit per Root Location, but default we don't $limitToRootLocation = $this->configResolver->getParameter('limit_to_rootlocation', 'nova_ezseo'); - if (true === $limitToRootLocation) { + if ((int) $rootLocationId) { + try { + $rootLocation = $this->repository->getLocationService()->loadLocation($rootLocationId); + } catch (NotFoundException|UnauthorizedException $e) { + $rootLocation = null; + } + if ($rootLocation) { + $criterions[] = new Criterion\Subtree($rootLocation->pathString); + } + } elseif (true === $limitToRootLocation) { $criterions[] = new Criterion\Subtree($this->getRootLocation()->pathString); } @@ -88,7 +101,8 @@ public function __invoke(): Query ) ); - $criterions[] = new Criterion\LanguageCode($this->configResolver->getParameter('languages'), true); + $languages = empty($languages) ? $this->configResolver->getParameter('languages') : $languages; + $criterions[] = new Criterion\LanguageCode($languages, $matchAlwaysAvailable); $query->query = new Criterion\LogicalAnd($criterions); $query->sortClauses = [new SortClause\DatePublished(Query::SORT_DESC)]; diff --git a/components/SEOBundle/bundle/Core/UrlWildcardRouter.php b/components/SEOBundle/bundle/Core/UrlWildcardRouter.php index cd1ab4668..491dfd240 100644 --- a/components/SEOBundle/bundle/Core/UrlWildcardRouter.php +++ b/components/SEOBundle/bundle/Core/UrlWildcardRouter.php @@ -33,7 +33,7 @@ public function matchRequest(Request $request): array { try { // Manage full url : http://host.com/uri - $requestedPath = $request->attributes->get('semanticPathinfo', $request->getPathInfo()); + $requestedPath = $request->getPathInfo(); $requestUriFull = $request->getSchemeAndHttpHost().$requestedPath; $urlWildcard = $this->wildcardService->translate($requestUriFull); } catch (Exception $e) { @@ -51,6 +51,7 @@ public function matchRequest(Request $request): array if (0 === strpos($urlWildcard->uri, 'http://') || 'https://' === substr($urlWildcard->uri, 0, 8)) { $params += ['semanticPathinfo' => trim($urlWildcard->uri, '/')]; + $params += ['prependSiteaccessOnRedirect' => false]; } else { $params += ['semanticPathinfo' => '/'.trim($urlWildcard->uri, '/')]; } diff --git a/components/SEOBundle/bundle/DependencyInjection/Configuration.php b/components/SEOBundle/bundle/DependencyInjection/Configuration.php index 7ff4c9cc5..e87da46ea 100644 --- a/components/SEOBundle/bundle/DependencyInjection/Configuration.php +++ b/components/SEOBundle/bundle/DependencyInjection/Configuration.php @@ -28,8 +28,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('google_gatracker')->defaultValue('~')->end() ->scalarNode('google_anonymizeIp')->defaultValue('~')->end() ->scalarNode('bing_verification')->defaultValue('~')->end() - ->booleanNode('limit_to_rootlocation')->defaultValue('~')->end() - ->booleanNode('display_images_in_sitemap')->defaultValue('~')->end() + ->booleanNode('limit_to_rootlocation')->defaultFalse()->end() + ->booleanNode('multi_siteaccess_sitemap')->defaultFalse()->end() + ->booleanNode('multi_languages_sitemap')->defaultFalse()->end() + ->booleanNode('display_images_in_sitemap')->defaultFalse()->end() ->scalarNode('fieldtype_metas_identifier')->defaultValue('metas')->end() ->arrayNode('fieldtype_metas') ->isRequired() diff --git a/components/SEOBundle/bundle/DependencyInjection/NovaeZSEOExtension.php b/components/SEOBundle/bundle/DependencyInjection/NovaeZSEOExtension.php index a91191aa8..5c1bd66bf 100644 --- a/components/SEOBundle/bundle/DependencyInjection/NovaeZSEOExtension.php +++ b/components/SEOBundle/bundle/DependencyInjection/NovaeZSEOExtension.php @@ -37,7 +37,9 @@ public function prepend(ContainerBuilder $container): void 'wildcard_routing.yml' => 'ibexa', 'ez_field_templates.yml' => 'ibexa', 'variations.yml' => 'ibexa', + 'ibexa.yaml' => 'ibexa', 'admin_ui/ez_field_templates.yml' => 'ibexa', + 'ibexa_locale_conversion.yml' => 'ibexa', ]; foreach ($configs as $fileName => $extensionName) { @@ -68,6 +70,8 @@ public function load(array $configs, ContainerBuilder $container): void $processor->mapSetting('bing_verification', $config); $processor->mapSetting('limit_to_rootlocation', $config); $processor->mapSetting('display_images_in_sitemap', $config); + $processor->mapSetting('multi_siteaccess_sitemap', $config); + $processor->mapSetting('multi_languages_sitemap', $config); $processor->mapSetting('robots', $config); $processor->mapConfigArray('fieldtype_metas', $config, ContextualizerInterface::MERGE_FROM_SECOND_LEVEL); $processor->mapConfigArray('default_metas', $config); diff --git a/components/SEOBundle/bundle/Form/Type/MetaType.php b/components/SEOBundle/bundle/Form/Type/MetaType.php index 4766a6df0..6d3e3f428 100644 --- a/components/SEOBundle/bundle/Form/Type/MetaType.php +++ b/components/SEOBundle/bundle/Form/Type/MetaType.php @@ -28,7 +28,7 @@ class MetaType extends AbstractType { protected SeoMetadataFieldTypeRegistry $metadataFieldTypeRegistry; - private ConfigResolverInterface $configResolver; + protected ConfigResolverInterface $configResolver; /** * Constructor. @@ -63,8 +63,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $novaEzseo = $this->configResolver->getParameter('fieldtype_metas', 'nova_ezseo'); if (isset($novaEzseo[$builder->getName()])) { $config = $novaEzseo[$builder->getName()]; - $type = $config['type']; - $options = array_merge($options, $config['params']); + $type = $config['type'] ?? $type; + if ('select' === $type) { + $options = array_merge($options, $config['params']); + } } $constraints = $this->getConstraints($config); diff --git a/components/SEOBundle/bundle/Resources/config/default_settings.yml b/components/SEOBundle/bundle/Resources/config/default_settings.yml index 9fed94a0a..92dfcd1b6 100644 --- a/components/SEOBundle/bundle/Resources/config/default_settings.yml +++ b/components/SEOBundle/bundle/Resources/config/default_settings.yml @@ -11,8 +11,10 @@ parameters: nova_ezseo.default.google_anonymizeIp: ~ nova_ezseo.default.bing_verification: ~ nova_ezseo.default.custom_fallback_service: ~ - nova_ezseo.default.limit_to_rootlocation: ~ - nova_ezseo.default.display_images_in_sitemap: ~ + nova_ezseo.default.limit_to_rootlocation: false + nova_ezseo.default.display_images_in_sitemap: false + nova_ezseo.default.multi_siteaccess_sitemap: false + nova_ezseo.default.multi_languages_sitemap: false nova_ezseo.default.sitemap_excludes: locations: [] subtrees: [] diff --git a/components/SEOBundle/bundle/Resources/config/ibexa.yaml b/components/SEOBundle/bundle/Resources/config/ibexa.yaml new file mode 100644 index 000000000..efa81ce57 --- /dev/null +++ b/components/SEOBundle/bundle/Resources/config/ibexa.yaml @@ -0,0 +1,7 @@ +orm: + entity_mappings: + NovaeZSEOBundle: + is_bundle: true + type: annotation + dir: Entity + prefix: Novactive\Bundle\eZSEOBundle\Entity \ No newline at end of file diff --git a/components/SEOBundle/bundle/Resources/config/ibexa_locale_conversion.yml b/components/SEOBundle/bundle/Resources/config/ibexa_locale_conversion.yml new file mode 100644 index 000000000..82877472d --- /dev/null +++ b/components/SEOBundle/bundle/Resources/config/ibexa_locale_conversion.yml @@ -0,0 +1,15 @@ +#list from https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +locale_conversion: + bre-BT: br_BT + rum-RO: ro_RO + mlt-MT: mt_MT + gle-IE: ga_IE + est-EE: et_EE + slv-SI: sl_SI + sla-MK: sl_MK + lav-LV: lv_LV + lit-LT: lt_LT + geo-GE: ka_GE + per-IR: fa_IR + bul-BG: bg_BG + bel-BY: be_BY diff --git a/components/SEOBundle/bundle/Resources/config/services.yml b/components/SEOBundle/bundle/Resources/config/services.yml index 5ccfcb341..79fc4caca 100644 --- a/components/SEOBundle/bundle/Resources/config/services.yml +++ b/components/SEOBundle/bundle/Resources/config/services.yml @@ -54,9 +54,9 @@ services: Novactive\Bundle\eZSEOBundle\Core\MetaNameSchema: lazy: true arguments: - $contentTypeHandler: "@Ibexa\\Contracts\\Core\\Persistence\\Content\\Type\\Handler" - $languageHandler: "@Ibexa\\Core\\Persistence\\Cache\\ContentLanguageHandler" - $translationHelper: "@Ibexa\\Core\\Helper\\TranslationHelper" + $contentTypeHandler: '@Ibexa\Contracts\Core\Persistence\Content\Type\Handler' + $languageHandler: '@Ibexa\Core\Persistence\Cache\ContentLanguageHandler' + $translationHelper: '@Ibexa\Core\Helper\TranslationHelper' calls: - [setRichTextConverter, ["@Ibexa\\FieldTypeRichText\\RichText\\Converter\\Html5"]] # Note: injecting lower layer Variation Handler (AliasGenerator) as a workaround for missing Public API objects context @@ -76,6 +76,8 @@ services: $ioService: '@ezseo_importurls.ibexa.core.io_service' $cacheDirectory: '%kernel.cache_dir%' + Novactive\Bundle\eZSEOBundle\Core\Helper\SiteMapHelper: + Novactive\Bundle\eZSEOBundle\Core\SiteAccessAwareEntityManagerFactory: arguments: $repositoryConfigurationProvider: "@Ibexa\\Bundle\\Core\\ApiLoader\\RepositoryConfigurationProvider" diff --git a/components/SEOBundle/ci-config.yaml b/components/SEOBundle/ci-config.yaml index 51d98c43c..0f380b380 100644 --- a/components/SEOBundle/ci-config.yaml +++ b/components/SEOBundle/ci-config.yaml @@ -1,2 +1,3 @@ install: true test: true +required_ibexa_version: ^4.6 diff --git a/components/SEOBundle/composer.json b/components/SEOBundle/composer.json index 8fde65e8d..1af78bafd 100644 --- a/components/SEOBundle/composer.json +++ b/components/SEOBundle/composer.json @@ -27,10 +27,11 @@ "MIT" ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "ext-dom": "*", "ext-pdo": "*", - "ext-json": "*" + "ext-json": "*", + "ibexa/core": "^4.6" }, "autoload": { "psr-4": { diff --git a/components/SEOBundle/documentation/USAGE.md b/components/SEOBundle/documentation/USAGE.md index 06214de91..fa2bcbef3 100644 --- a/components/SEOBundle/documentation/USAGE.md +++ b/components/SEOBundle/documentation/USAGE.md @@ -141,6 +141,48 @@ nova_ezseo: contentTypeIdentifiers: ['footer','something'] ``` + +Set `multi_siteaccess_sitemap` to true to automatically generate the index page sitemap.xml per site access. + +```yml +nova_ezseo: + system: + default: + multi_siteaccess_sitemap: true +``` + +```html + + + https://www.example.com/fr/sitemap-1.xml + 2024-05-31T01:04:52+00:00 + + + https://www.example.com/en/sitemap-1.xml + 2024-05-31T01:04:52+00:00 + + +``` + +Set `multi_languages_sitemap` to inject the multilingual and multinational site annotations tag +Notice: this doesn't work with `multi_siteaccess_sitemap: false`. +```html + + https://www.example.com/pageFr + + + 2024-05-03T08:48:52+00:00 + +``` + +```yml +nova_ezseo: + system: + default: + multi_siteaccess_sitemap: true + multi_languages_sitemap: true +``` + Set "display_images_in_sitemap" to true to inject the image tags. Notice: this doesn't work with `limit_to_rootlocation: true`. diff --git a/components/SamlBundle/README.md b/components/SamlBundle/README.md index bbc86170f..7555aebd2 100644 --- a/components/SamlBundle/README.md +++ b/components/SamlBundle/README.md @@ -47,6 +47,32 @@ env(SAML_IDENTITY_PROVIDER_EMAIL_ATTRIBUTE): ~ env(SAML_IDENTITY_PROVIDER_LOGIN_ATTRIBUTE): ~ ``` +These variables are used to define the following global configuration : +``` +idp: + entityId: '%env(resolve:SAML_IDENTITY_PROVIDER_ENTITYID)%' + singleSignOnService: + url: '%env(resolve:SAML_IDENTITY_PROVIDER_LOGIN_URL)%' + binding: '%env(resolve:SAML_IDENTITY_PROVIDER_LOGIN_BINDING)%' + singleLogoutService: + url: '%env(resolve:SAML_IDENTITY_PROVIDER_LOGOUT_URL)%' + binding: '%env(resolve:SAML_IDENTITY_PROVIDER_LOGOUT_BINDING)%' + x509cert: '%env(resolve:SAML_IDENTITY_PROVIDER_X509_CERT)%' +sp: + entityId: '%env(resolve:SAML_SERVICE_PROVIDER_URL)%/saml/metadata' + assertionConsumerService: + url: '%env(resolve:SAML_SERVICE_PROVIDER_URL)%/saml/acs' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + singleLogoutService: + url: '%env(resolve:SAML_SERVICE_PROVIDER_URL)%/saml/logout' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + NameIDFormat: '%env(resolve:SAML_SERVICE_PROVIDER_NAMEID_FORMAT)%' +baseurl: '%env(resolve:SAML_SERVICE_PROVIDER_URL)%/saml' +debug: '%kernel.debug%' +``` + +To change the configuration based on siteaccess, it's possible to defined it under the folowing siteaccess aware parameter : `almaviacx.saml..auth_settings` + The following parameters are also available to tweak the behavior ```yaml # Attribute used to get the email address from @@ -78,6 +104,4 @@ almaviacx.saml.identity.provider.login.attribute: # Change the user load method almaviacx.saml.config.default.user_load_method: !php/const AlmaviaCX\Bundle\IbexaSaml\Security\Saml\SamlUserProvider::LOAD_METHOD_EMAIL - - ``` diff --git a/components/SamlBundle/src/bundle/AlmaviaCXIbexaSamlBundle.php b/components/SamlBundle/src/bundle/AlmaviaCXIbexaSamlBundle.php index a5b2e557f..b7854195d 100644 --- a/components/SamlBundle/src/bundle/AlmaviaCXIbexaSamlBundle.php +++ b/components/SamlBundle/src/bundle/AlmaviaCXIbexaSamlBundle.php @@ -4,8 +4,15 @@ namespace AlmaviaCX\Bundle\IbexaSamlBundle; +use AlmaviaCX\Bundle\IbexaSamlBundle\DependencyInjection\Compiler\LazySaml2Auth; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class AlmaviaCXIbexaSamlBundle extends Bundle { + public function build(ContainerBuilder $container) + { + parent::build($container); + $container->addCompilerPass(new LazySaml2Auth()); + } } diff --git a/components/SamlBundle/src/bundle/DependencyInjection/Compiler/LazySaml2Auth.php b/components/SamlBundle/src/bundle/DependencyInjection/Compiler/LazySaml2Auth.php new file mode 100644 index 000000000..e5bc8ae66 --- /dev/null +++ b/components/SamlBundle/src/bundle/DependencyInjection/Compiler/LazySaml2Auth.php @@ -0,0 +1,25 @@ +hasDefinition(Auth::class)) { + return; + } + + $serviceDefinition = $container->getDefinition(Auth::class); + $serviceDefinition->setFactory(new Reference(SamlAuthFactory::class)); + $serviceDefinition->setLazy(true); + } +} diff --git a/components/SamlBundle/src/bundle/Resources/config/services.yaml b/components/SamlBundle/src/bundle/Resources/config/services.yaml index ea2da864a..080e10f08 100644 --- a/components/SamlBundle/src/bundle/Resources/config/services.yaml +++ b/components/SamlBundle/src/bundle/Resources/config/services.yaml @@ -1,5 +1,4 @@ services: - AlmaviaCX\Bundle\IbexaSaml\Security\Saml\SamlExceptionLogger: arguments: - '@monolog.logger.saml' @@ -31,7 +30,7 @@ services: $userService: '@ibexa.api.service.user' $configResolver: '@ibexa.config.resolver' - # override to make it lazy - OneLogin\Saml2\Auth: - arguments: [ '%hslavich_onelogin_saml.settings%' ] - lazy: true + AlmaviaCX\Bundle\IbexaSaml\Security\Saml\SamlAuthFactory: + arguments: + $configResolver: '@Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface' + $defaultSettings: '%hslavich_onelogin_saml.settings%' diff --git a/components/SamlBundle/src/lib/Security/Saml/SamlAuthFactory.php b/components/SamlBundle/src/lib/Security/Saml/SamlAuthFactory.php new file mode 100644 index 000000000..eb9ca8d3d --- /dev/null +++ b/components/SamlBundle/src/lib/Security/Saml/SamlAuthFactory.php @@ -0,0 +1,51 @@ +defaultSettings = $defaultSettings; + $this->configResolver = $configResolver; + } + + public function __invoke(): Auth + { + $settings = $this->defaultSettings; + try { + $saSettings = $this->configResolver->getParameter('auth_settings', 'almaviacx.saml'); + } catch (ParameterNotFoundException $exception) { + $saSettings = []; + } + + return new Auth($this->mergeSettings($settings, $saSettings)); + } + + protected function mergeSettings(array $defaultSettings, array $settings): array + { + foreach ($defaultSettings as $key => $setting) { + if (!isset($settings[$key])) { + continue; + } + if (is_array($setting)) { + $defaultSettings[$key] = $this->mergeSettings($defaultSettings[$key], $settings[$key]); + } else { + $defaultSettings[$key] = $settings[$key]; + } + } + + return $defaultSettings; + } +} diff --git a/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.en.yml b/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.en.yml new file mode 100644 index 000000000..7b03f566d --- /dev/null +++ b/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.en.yml @@ -0,0 +1,2 @@ +solr_admin: Solr +solr_admin.resources: Manage Synonyms/Stopwords \ No newline at end of file diff --git a/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml b/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml new file mode 100644 index 000000000..93b64e1da --- /dev/null +++ b/components/SolrSearchExtraBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml @@ -0,0 +1,2 @@ +solr_admin: Solr +solr_admin.resources: Gestion des synonymes/mots vides diff --git a/components/SolrSearchExtraBundle/src/lib/Query/Content/CriterionVisitor/FilterTag.php b/components/SolrSearchExtraBundle/src/lib/Query/Content/CriterionVisitor/FilterTag.php index aac8442fb..b0ec2bc7d 100644 --- a/components/SolrSearchExtraBundle/src/lib/Query/Content/CriterionVisitor/FilterTag.php +++ b/components/SolrSearchExtraBundle/src/lib/Query/Content/CriterionVisitor/FilterTag.php @@ -26,7 +26,8 @@ public function canVisit(Criterion $criterion): bool */ public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null): string { - $stringQuery = strtr($subVisitor->visit($criterion->criterion), ['(' => '', ')' => '']); + $stringQuery = $subVisitor->visit($criterion->criterion); + $stringQuery = trim($stringQuery, '()'); return '{!tag='.$criterion->tag.'}('.$stringQuery.')'; } diff --git a/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.en.yml b/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.en.yml new file mode 100644 index 000000000..60326cea0 --- /dev/null +++ b/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.en.yml @@ -0,0 +1 @@ +ibexa.translation.ui.label: Translations UI \ No newline at end of file diff --git a/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml b/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml new file mode 100644 index 000000000..2e107d998 --- /dev/null +++ b/components/TranslationUiBundle/src/bundle/Resources/translations/ibexa_menu.fr.yml @@ -0,0 +1 @@ +ibexa.translation.ui.label: Gestion des traductions diff --git a/composer.json b/composer.json index 33bbc869b..a790ed51e 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "knplabs/knp-menu": "^3.1", "behat/behat": "^3.8", "friends-of-behat/mink-extension": "^2.5", - "ext-pdo": "*" + "ext-pdo": "*", + "ext-dom": "*" } }