diff --git a/documentation/development.md b/documentation/development.md index 0bfecacae..869427a70 100644 --- a/documentation/development.md +++ b/documentation/development.md @@ -229,7 +229,7 @@ The update hook above will re-import all configuration from `helfi_media` module The `helfi_platform_config.config_update_helper` invokes `hook_rewrite_config_update`, which allows custom modules to react to config re-importing. -##### In this example we would want to override Text paragraph label with a configuration found in my_module. +##### In this example we would want to override Text paragraph label with a configuration found in my_module. To trigger the `hook_rewrite_config_update`, implement the hook to your `my_module.module`: ```php @@ -254,3 +254,54 @@ label: Teksti (ylikirjoitettu) ``` +## Tokens + +Helfi platform config implements `hook_tokens()`. With the help of `helfi_platform_config.og_image_manager` service, it provides `[*:shareable-image]` token. Modules may implement services that handle this token for their entity types. + +Modules that use this system should still implement `hook_tokens_info` to provide information about the implemented token. + +### Defining image builder service + +Add a new service: + +```yml +# yourmodule/yourmodule.services.yml + yourmodule.og_image.your_entity_type: + class: Drupal\yourmodule\Token\YourEntityImageBuilder + arguments: [] + tags: + - { name: helfi_platform_config.og_image_builder, priority: 100 } +``` + +```php +# yourmodule/src/Token/YourEntityImageBuilder.php +field_image->entity->getFileUri(); + } + +} +``` diff --git a/fixtures/og-global-sv.png b/fixtures/og-global-sv.png new file mode 100644 index 000000000..a20c54342 Binary files /dev/null and b/fixtures/og-global-sv.png differ diff --git a/fixtures/og-global.png b/fixtures/og-global.png new file mode 100644 index 000000000..2e17f3d9d Binary files /dev/null and b/fixtures/og-global.png differ diff --git a/helfi_platform_config.module b/helfi_platform_config.module index 4dcdbbaa3..28b3452e6 100644 --- a/helfi_platform_config.module +++ b/helfi_platform_config.module @@ -17,7 +17,6 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldConfigBase; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Link; -use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; @@ -289,17 +288,6 @@ function helfi_platform_config_block_access(Block $block, $operation, AccountInt return AccessResult::neutral(); } -/** - * Implements hook_token_info(). - */ -function helfi_platform_config_token_info() : array { - $info['tokens']['site']['page-title-suffix'] = [ - 'name' => t('Page title suffix'), - 'description' => t('Official suffix for page title.'), - ]; - return $info; -} - /** * Grants permissions for given role. * @@ -337,35 +325,6 @@ function helfi_platform_config_remove_permissions_from_all_roles(array $permissi } } -/** - * Implements hook_tokens(). - */ -function helfi_platform_config_tokens( - $type, - $tokens, - array $data, - array $options, - BubbleableMetadata $bubbleable_metadata -): array { - $replacements = []; - - foreach ($tokens as $name => $original) { - if ($name === 'page-title-suffix') { - $language = Drupal::languageManager() - ->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE); - - $replacements[$original] = match ($language->getId()) { - 'fi' => 'Helsingin kaupunki', - 'sv' => 'Helsingfors stad', - 'ru' => 'Гopoд Xeльcинки', - default => 'City of Helsinki', - }; - } - } - - return $replacements; -} - /** * Implements hook_system_breadcrumb_alter(). */ diff --git a/helfi_platform_config.services.yml b/helfi_platform_config.services.yml index c1220b744..8fe795bba 100644 --- a/helfi_platform_config.services.yml +++ b/helfi_platform_config.services.yml @@ -60,3 +60,19 @@ services: arguments: - 'helfi_platform_config' + helfi_platform_config.og_image_manager: + class: Drupal\helfi_platform_config\Token\OGImageManager + arguments: + - '@module_handler' + - '@file_url_generator' + tags: + - { name: service_collector, call: add, tag: helfi_platform_config.og_image_builder } + + helfi_platform_config.og_image.default: + class: Drupal\helfi_platform_config\Token\DefaultImageBuilder + arguments: + - '@module_handler' + - '@language_manager' + - '@file_url_generator' + tags: + - { name: helfi_platform_config.og_image_builder, priority: -100 } diff --git a/helfi_platform_config.tokens.inc b/helfi_platform_config.tokens.inc new file mode 100644 index 000000000..72a9c0370 --- /dev/null +++ b/helfi_platform_config.tokens.inc @@ -0,0 +1,120 @@ + t('Default OG Image'), + 'description' => t('Default OG image is used as a default thumbnail in social networks and other services.'), + ]; + $info['tokens']['node']['shareable-image'] = [ + 'name' => t('Shareable image'), + 'description' => t('Shareable image is used as a thumbnail in social networks and other services.'), + ]; + $info['tokens']['site']['page-title-suffix'] = [ + 'name' => t('Page title suffix'), + 'description' => t('Official suffix for page title.'), + ]; + + $info['tokens']['node']['lead-in'] = [ + 'name' => t('Lead in'), + 'description' => t( + 'Lead in will try to use the hero paragraph description if it exists. If not, it will use the node lead in field.' + ), + ]; + return $info; +} + +/** + * Implements hook_tokens(). + * + * @see \Drupal\helfi_platform_config\Token\OGImageManager + */ +function helfi_platform_config_tokens( + $type, + $tokens, + array $data, + array $options, + BubbleableMetadata $bubbleable_metadata +) : array { + $replacements = []; + + foreach ($tokens as $name => $original) { + if ($name === 'shareable-image') { + $entity = $data[$type] ?? NULL; + + if ($entity === NULL || $entity instanceof EntityInterface) { + /** @var \Drupal\helfi_platform_config\Token\OGImageManager $image_manager */ + $image_manager = \Drupal::service('helfi_platform_config.og_image_manager'); + + $replacements[$original] = $image_manager->buildUrl($entity); + } + } + elseif ($name === 'page-title-suffix') { + $language = Drupal::languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE); + + $replacements[$original] = match ($language->getId()) { + 'fi' => 'Helsingin kaupunki', + 'sv' => 'Helsingfors stad', + 'ru' => 'Гopoд Xeльcинки', + default => 'City of Helsinki', + }; + } + // Custom token for lead in. + elseif ($name === 'lead-in' && !empty($data['node'])) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $data['node']; + $lead_in_text = ''; + + // Check if lead in field exists. + if ( + $node->hasField('field_lead_in') && + !$node?->get('field_lead_in')?->isEmpty() + ) { + // Use lead in field as lead in text. + $lead_in_text = $node->get('field_lead_in')->value; + } + + // Check if hero paragraph and hero paragraph description exists. + if ( + $node->hasField('field_hero') && + !$node->get('field_hero')?->first()?->isEmpty() + ) { + // Get hero paragraph. + $hero = $node->get('field_hero') + ?->first() + ?->get('entity') + ?->getTarget() + ?->getValue(); + + if ( + $hero instanceof ParagraphInterface && + $hero->hasField('field_hero_desc') && + !$hero->get('field_hero_desc')->isEmpty() + ) { + // Use hero paragraph description as lead in text. + $lead_in_text = $hero->get('field_hero_desc')->value; + } + } + + // Add lead in text to replacements. + $replacements[$original] = $lead_in_text; + } + } + + return $replacements; +} diff --git a/modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc b/modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc deleted file mode 100644 index 1b7417a59..000000000 --- a/modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc +++ /dev/null @@ -1,171 +0,0 @@ - t('Default OG Image'), - 'description' => t('Default OG image is used as a default thumbnail in social networks and other services.'), - ]; - - $info['tokens']['node']['shareable-image'] = [ - 'name' => t('Shareable image'), - 'description' => t('Shareable image is used as a thumbnail in social networks and other services.'), - ]; - - $info['tokens']['node']['lead-in'] = [ - 'name' => t('Lead in'), - 'description' => t( - 'Lead in will try to use the hero paragraph description if it exists. If not, it will use the node lead in field.' - ), - ]; - - return $info; -} - -/** - * Implements hook_tokens(). - */ -function hdbt_admin_tools_tokens( - $type, - $tokens, - array $data, - array $options, - BubbleableMetadata $bubbleable_metadata -) { - $replacements = []; - - foreach ($tokens as $name => $original) { - $default_image = ''; - - /** @var Drupal\Core\Extension\ThemeHandler $theme_handler */ - $theme_handler = Drupal::service('theme_handler'); - - // Add default og-image as the shareable image. - if ($theme_handler->themeExists('hdbt')) { - $theme = $theme_handler->getTheme('hdbt'); - $current_language = \Drupal::languageManager() - ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); - $image_file_name = $current_language === 'sv' ? 'og-global-sv.png' : 'og-global.png'; - - /** @var \Drupal\Core\File\FileUrlGeneratorInterface $service */ - $service = \Drupal::service('file_url_generator'); - $default_image = $service->generate("{$theme->getPath()}/src/images/{$image_file_name}") - ->toString(TRUE) - ->getGeneratedUrl(); - } - - // Custom token for default-og-image. - if ($name === 'default-og-image') { - $replacements[$original] = $default_image; - } - - // Handle fallback image for TPR Unit. - if ($name === 'picture' && $type === 'tpr_unit' && !empty($data[$type])) { - /** @var \Drupal\helfi_tpr\Entity\Unit $entity */ - $entity = $data[$type]; - $replacements[$original] = $entity->getPictureUrl() ?? $default_image; - } - - // Custom token for shareable-image. - if ($name === 'shareable-image' && !empty($data['node'])) { - /** @var \Drupal\node\NodeInterface $node */ - $node = $data['node']; - $image_url = $default_image; - - $image_style = ImageStyle::load('og_image'); - - if ( - $node->hasField('field_liftup_image') && - isset($node->field_liftup_image->entity) && - $node->field_liftup_image->entity instanceof MediaInterface && - $node->field_liftup_image->entity->hasField('field_media_image') - ) { - // If liftup image has an image set, use it as the shareable image. - $image_entity = $node->field_liftup_image->entity->field_media_image; - $image_path = $image_entity->entity->getFileUri(); - $image_url = $image_style->buildUrl($image_path); - } - elseif ( - $node->hasField('field_image') && - isset($node->field_image->entity) && - $node->field_image->entity instanceof MediaInterface && - $node->field_image->entity->hasField('field_media_image') - ) { - // If the node has an image, use that. - $image_entity = $node->field_image->entity->field_media_image; - $image_path = $image_entity->entity->getFileUri(); - $image_url = $image_style->buildUrl($image_path); - } - elseif ( - $node->hasField('field_organization') && - $node->get('field_organization')?->entity?->hasField('field_default_image') && - !$node->get('field_organization')->entity->get('field_default_image')->isEmpty() - ) { - // Use the image from the taxonomy term. - $taxonomy_term = $node->field_organization->entity; - $image_entity = $taxonomy_term->field_default_image; - $image_path = $image_entity->entity->getFileUri(); - $image_url = $image_style->buildUrl($image_path); - } - - $replacements[$original] = $image_url; - } - - // Custom token for lead in. - if ($name === 'lead-in' && !empty($data['node'])) { - /** @var \Drupal\node\NodeInterface $node */ - $node = $data['node']; - $lead_in_text = ''; - - // Check if lead in field exists. - if ( - $node->hasField('field_lead_in') && - !$node?->get('field_lead_in')?->isEmpty() - ) { - // Use lead in field as lead in text. - $lead_in_text = $node->get('field_lead_in')->value; - } - - // Check if hero paragraph and hero paragraph description exists. - if ( - $node->hasField('field_hero') && - !$node->get('field_hero')?->first()?->isEmpty() - ) { - // Get hero paragraph. - $hero = $node->get('field_hero') - ?->first() - ?->get('entity') - ?->getTarget() - ?->getValue(); - - if ( - $hero instanceof ParagraphInterface && - $hero->hasField('field_hero_desc') && - !$hero->get('field_hero_desc')->isEmpty() - ) { - // Use hero paragraph description as lead in text. - $lead_in_text = $hero->get('field_hero_desc')->value; - } - } - - // Add lead in text to replacements. - $replacements[$original] = $lead_in_text; - } - } - - return $replacements; -} diff --git a/modules/helfi_base_content/config/rewrite/metatag.metatag_defaults.global.yml b/modules/helfi_base_content/config/rewrite/metatag.metatag_defaults.global.yml index 0a288798a..1f93ed46a 100644 --- a/modules/helfi_base_content/config/rewrite/metatag.metatag_defaults.global.yml +++ b/modules/helfi_base_content/config/rewrite/metatag.metatag_defaults.global.yml @@ -9,6 +9,6 @@ tags: twitter_cards_page_url: '[current-page:url]' twitter_cards_title: '[current-page:title] | [site:page-title-suffix]' twitter_cards_type: summary_large_image - og_image: '[site:default-og-image]' - twitter_cards_image: '[site:default-og-image]' + og_image: '[site:shareable-image]' + twitter_cards_image: '[site:shareable-image]' og_site_name: '[site:page-title-suffix]' diff --git a/modules/helfi_base_content/helfi_base_content.install b/modules/helfi_base_content/helfi_base_content.install index 8b43fc4d3..45238b1b2 100644 --- a/modules/helfi_base_content/helfi_base_content.install +++ b/modules/helfi_base_content/helfi_base_content.install @@ -311,3 +311,11 @@ function helfi_base_content_update_9007() : void { } } } + +/** + * UHF-9081 Update og_description and description meta tags. + */ +function helfi_base_content_update_9008() : void { + \Drupal::service('helfi_platform_config.config_update_helper') + ->update('helfi_base_content'); +} diff --git a/modules/helfi_image_styles/helfi_image_styles.module b/modules/helfi_image_styles/helfi_image_styles.module index d1e47fc69..0b2e27ad4 100644 --- a/modules/helfi_image_styles/helfi_image_styles.module +++ b/modules/helfi_image_styles/helfi_image_styles.module @@ -7,6 +7,8 @@ declare(strict_types=1); +use Drupal\Component\Utility\UrlHelper; + /** * Implements hook_modules_installed(). */ @@ -28,3 +30,20 @@ function helfi_image_styles_modules_installed(array $modules) : void { $moduleInstaller->install(['imagemagick']); } } + +/** + * Implements hook_og_image_uri_alter(). + * + * @see \Drupal\helfi_platform_config\Token\OGImageManager::buildUrl() + */ +function helfi_image_styles_og_image_uri_alter(&$image_uri) : void { + // Apply image style to internal uris. + if (!UrlHelper::isExternal($image_uri)) { + /** @var \Drupal\image\Entity\ImageStyle $image_style */ + $image_style = \Drupal::entityTypeManager() + ->getStorage('image_style') + ->load('og_image'); + + $image_uri = $image_style->buildUrl($image_uri); + } +} diff --git a/modules/helfi_platform_config_base/helfi_platform_config_base.services.yml b/modules/helfi_platform_config_base/helfi_platform_config_base.services.yml new file mode 100644 index 000000000..b56afd7cc --- /dev/null +++ b/modules/helfi_platform_config_base/helfi_platform_config_base.services.yml @@ -0,0 +1,7 @@ +services: + helfi_platform_config_base.og_image.node: + class: Drupal\helfi_platform_config_base\Token\NodeImageBuilder + arguments: + - '@entity_type.manager' + tags: + - { name: helfi_platform_config.og_image_builder } diff --git a/modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php b/modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php new file mode 100644 index 000000000..f320e3ee6 --- /dev/null +++ b/modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php @@ -0,0 +1,85 @@ +getImage($entity)) { + return $image_file->getFileUri(); + } + + return NULL; + } + + /** + * Get shareable image from node entity. + * + * @param \Drupal\node\NodeInterface $node + * Node. + * + * @return \Drupal\file\FileInterface|null + * Image entity. + */ + private function getImage(NodeInterface $node) : ?FileInterface { + if ( + $node->hasField('field_liftup_image') && + isset($node->field_liftup_image->entity) && + $node->field_liftup_image->entity instanceof MediaInterface && + $node->field_liftup_image->entity->hasField('field_media_image') + ) { + // If liftup image has an image set, use it as the shareable image. + $file = $node->field_liftup_image->entity->field_media_image->entity; + assert($file instanceof FileInterface); + return $file; + } + elseif ( + $node->hasField('field_image') && + isset($node->field_image->entity) && + $node->field_image->entity instanceof MediaInterface && + $node->field_image->entity->hasField('field_media_image') + ) { + // If the node has an image, use that. + $file = $node->field_image->entity->field_media_image->entity; + assert($file instanceof FileInterface); + return $file; + } + elseif ( + $node->hasField('field_organization') && + $node->get('field_organization')?->entity?->hasField('field_default_image') && + !$node->get('field_organization')->entity->get('field_default_image')->isEmpty() + ) { + // Use the image from the taxonomy term. + $taxonomy_term = $node->field_organization->entity; + $file = $taxonomy_term->field_default_image->entity; + assert($file instanceof FileInterface); + return $file; + } + + return NULL; + } + +} diff --git a/modules/helfi_tpr_config/config/install/metatag.metatag_defaults.tpr_unit.yml b/modules/helfi_tpr_config/config/install/metatag.metatag_defaults.tpr_unit.yml index cfbc08b0d..e1ba9e20d 100644 --- a/modules/helfi_tpr_config/config/install/metatag.metatag_defaults.tpr_unit.yml +++ b/modules/helfi_tpr_config/config/install/metatag.metatag_defaults.tpr_unit.yml @@ -12,7 +12,7 @@ tags: article_published_time: '[tpr_unit:created:html_datetime]' og_description: '[tpr_unit:description:summary]' og_email: '[tpr_unit:email]' - og_image: '[tpr_unit:picture]' + og_image: '[tpr_unit:shareable-image]' og_latitude: '[tpr_unit:latitude]' og_longitude: '[tpr_unit:longitude]' og_phone_number: '[tpr_unit:phone]' diff --git a/modules/helfi_tpr_config/helfi_tpr_config.info.yml b/modules/helfi_tpr_config/helfi_tpr_config.info.yml index f0399ea16..53490d271 100644 --- a/modules/helfi_tpr_config/helfi_tpr_config.info.yml +++ b/modules/helfi_tpr_config/helfi_tpr_config.info.yml @@ -9,6 +9,7 @@ dependencies: - 'helfi_base_content:helfi_base_content' - 'helfi_media:helfi_media' - 'helfi_paragraphs_content_liftup:helfi_paragraphs_content_liftup' + - 'helfi_image_styles:helfi_image_styles' - 'helfi_tpr:helfi_tpr' - 'imagecache_external:imagecache_external' - 'views:views' diff --git a/modules/helfi_tpr_config/helfi_tpr_config.install b/modules/helfi_tpr_config/helfi_tpr_config.install index 4716268ad..d67cd1d0a 100644 --- a/modules/helfi_tpr_config/helfi_tpr_config.install +++ b/modules/helfi_tpr_config/helfi_tpr_config.install @@ -397,3 +397,12 @@ function helfi_tpr_config_update_9057(): void { \Drupal::service('helfi_platform_config.config_update_helper') ->update('helfi_tpr_config'); } + +/** + * UHF-9712: Unify metatags with platform config. + */ +function helfi_tpr_config_update_9058(): void { + // Re-import 'helfi_tpr_config' configuration. + \Drupal::service('helfi_platform_config.config_update_helper') + ->update('helfi_tpr_config'); +} diff --git a/modules/helfi_tpr_config/helfi_tpr_config.module b/modules/helfi_tpr_config/helfi_tpr_config.module index 20176b2b2..20e6c7d97 100644 --- a/modules/helfi_tpr_config/helfi_tpr_config.module +++ b/modules/helfi_tpr_config/helfi_tpr_config.module @@ -15,6 +15,7 @@ use Drupal\Core\Url; use Drupal\helfi_platform_config\DTO\ParagraphTypeCollection; use Drupal\helfi_tpr_config\Entity\ServiceList; use Drupal\helfi_tpr_config\Entity\ServiceListSearch; +use Drupal\helfi_tpr_config\Entity\Unit; use Drupal\helfi_tpr_config\Entity\UnitSearch; use Drupal\linkit\Entity\Profile; use Drupal\paragraphs\Entity\Paragraph; @@ -23,6 +24,9 @@ use Drupal\paragraphs\Entity\Paragraph; * Implements hook_entity_bundle_info_alter(). */ function helfi_tpr_config_entity_bundle_info_alter(array &$bundles): void { + if (isset($bundles['tpr_unit']['tpr_unit'])) { + $bundles['tpr_unit']['tpr_unit']['class'] = Unit::class; + } if (isset($bundles['paragraph']['service_list'])) { $bundles['paragraph']['service_list']['class'] = ServiceList::class; } diff --git a/modules/helfi_tpr_config/helfi_tpr_config.services.yml b/modules/helfi_tpr_config/helfi_tpr_config.services.yml new file mode 100644 index 000000000..2c34af132 --- /dev/null +++ b/modules/helfi_tpr_config/helfi_tpr_config.services.yml @@ -0,0 +1,5 @@ +services: + helfi_tpr_config.og_image.tpr_entity: + class: Drupal\helfi_tpr_config\Token\UnitImageBuilder + tags: + - { name: helfi_platform_config.og_image_builder } diff --git a/modules/helfi_tpr_config/src/Entity/Unit.php b/modules/helfi_tpr_config/src/Entity/Unit.php new file mode 100644 index 000000000..af2add89e --- /dev/null +++ b/modules/helfi_tpr_config/src/Entity/Unit.php @@ -0,0 +1,42 @@ +get('picture_url_override')->entity; + + if (!$picture_url) { + $url = $this->get('picture_url')->value; + + // Run url through imagecache_external so that it is possible + // to apply image styles later. This method is in a bundle class + // so that helfi_tpr does not have to add dependency to + // imagecache_external. + return $url ? imagecache_external_generate_path($url) : NULL; + } + + if ($file = $picture_url->get('field_media_image')->entity) { + /** @var \Drupal\file\FileInterface $file */ + return $file->getFileUri(); + } + + return NULL; + } + +} diff --git a/modules/helfi_tpr_config/src/Token/UnitImageBuilder.php b/modules/helfi_tpr_config/src/Token/UnitImageBuilder.php new file mode 100644 index 000000000..ebfcbd6f6 --- /dev/null +++ b/modules/helfi_tpr_config/src/Token/UnitImageBuilder.php @@ -0,0 +1,32 @@ +getPictureUri(); + } + +} diff --git a/modules/helfi_tpr_config/tests/src/Functional/EntityOgImageTest.php b/modules/helfi_tpr_config/tests/src/Functional/EntityOgImageTest.php new file mode 100644 index 000000000..2c10b1308 --- /dev/null +++ b/modules/helfi_tpr_config/tests/src/Functional/EntityOgImageTest.php @@ -0,0 +1,111 @@ +getTestFiles('image')[0]->uri; + + $file = File::create([ + 'uri' => $uri, + ]); + $file->save(); + + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'Custom name', + 'field_media_image' => $file->id(), + ]); + $media->save(); + + $node = $this->drupalCreateNode([ + 'title' => 'title', + 'langcode' => 'fi', + 'bundle' => 'page', + 'status' => 1, + ]); + $node->save(); + + // Global image style is used when media field is not set. + $this->drupalGet($node->toUrl('canonical')); + $this->assertGlobalOgImage('fi'); + + // Media is used when 'field_liftup_image' is set. + $node->set('field_liftup_image', $media->id()); + $node->save(); + $this->drupalGet($node->toUrl('canonical')); + $this->assertImageStyle(); + + $unit = Unit::create([ + 'id' => 123, + 'title' => 'title', + 'langcode' => 'sv', + 'bundle' => 'tpr_unit', + ]); + $unit->save(); + + // Global image style is used when media field is not set. + $this->drupalGet($unit->toUrl('canonical')); + $this->assertGlobalOgImage('sv'); + + // Picture url override is used. + $unit->set('picture_url_override', $media->id()); + $unit->save(); + $this->drupalGet($unit->toUrl('canonical')); + $this->assertImageStyle(); + } + + /** + * Assert that og_image image style was used. + */ + private function assertImageStyle() : void { + $this->assertSession()->elementAttributeContains('css', 'meta[property="og:image"]', 'content', 'styles/og_image'); + } + + /** + * Assert that global og image was used. + * + * @param string $langcode + * Content langcode. + */ + private function assertGlobalOgImage(string $langcode) : void { + $og_image_file = match($langcode) { + 'sv' => 'og-global-sv.png', + default => 'og-global.png', + }; + + $this->assertSession()->elementAttributeContains('css', 'meta[property="og:image"]', 'content', $og_image_file); + $this->assertSession()->elementAttributeNotContains('css', 'meta[property="og:image"]', 'content', 'styles/og_image'); + } + +} diff --git a/phpstan.neon b/phpstan.neon index 7b326a702..ed41ffc90 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -52,33 +52,33 @@ parameters: path: modules/hdbt_admin_tools/src/Controller/ListController.php - message: '#^Access to an undefined property Drupal\\media\\MediaInterface::\$field_media_image.#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php reportUnmatched: false - message: '#^Access to an undefined property Drupal\\node\\NodeInterface::\$field_organization.#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php reportUnmatched: false - message: '#^Access to an undefined property Drupal\\Core\\Entity\\EntityInterface::\$field_default_image.#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php reportUnmatched: false - message: '#^Call to an undefined method Drupal\\Core\\Entity\\EntityInterface::getFileUri\(\).#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php reportUnmatched: false - message: '#^Call to an undefined method Drupal\\Core\\Entity\\EntityInterface::getFileUri\(\).#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php reportUnmatched: false - message: '#^Call to an undefined method Drupal\\Core\\Entity\\EntityInterface::hasField\(\).#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php - message: '#^Call to an undefined method Drupal\\Core\\Entity\\EntityInterface::get\(\).#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: modules/helfi_platform_config_base/src/Token/NodeImageBuilder.php - message: '#^Call to an undefined method Drupal\\Core\\TypedData\\TypedDataInterface::getTarget\(\).#' - path: modules/hdbt_admin_tools/hdbt_admin_tools.tokens.inc + path: helfi_platform_config.tokens.inc - message: '#^Call to an undefined method Drupal\\Core\\Field\\FieldDefinitionInterface::save\(\).#' path: helfi_platform_config.module diff --git a/src/Token/DefaultImageBuilder.php b/src/Token/DefaultImageBuilder.php new file mode 100644 index 000000000..e9517c3b9 --- /dev/null +++ b/src/Token/DefaultImageBuilder.php @@ -0,0 +1,49 @@ +moduleHandler->getModule('helfi_platform_config'); + $current_language = $this->languageManager + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + + $image_file_name = $current_language === 'sv' ? 'og-global-sv.png' : 'og-global.png'; + + return $this->fileUrlGenerator + ->generateAbsoluteString("{$module->getPath()}/fixtures/{$image_file_name}"); + } + +} diff --git a/src/Token/OGImageBuilderInterface.php b/src/Token/OGImageBuilderInterface.php new file mode 100644 index 000000000..546ab89bb --- /dev/null +++ b/src/Token/OGImageBuilderInterface.php @@ -0,0 +1,36 @@ +builders[$priority][] = $builder; + $this->sortedBuilders = []; + } + + /** + * Builds image url for given entity. + * + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * The entity. + * + * @return string|null + * OG image url or NULL on failure. + */ + public function buildUrl(?EntityInterface $entity) : ?string { + $image_uri = NULL; + + foreach ($this->getBuilders() as $builder) { + if ($builder->applies($entity)) { + // Replace the return value only if buildUri return non-NULL values. + // This allows previous image builders to provide default images + // in case field value is missing etc. + if ($uri = $builder->buildUri($entity)) { + $image_uri = $uri; + } + } + } + + if (!$image_uri) { + return NULL; + } + + // Let modules alter the final uri (like apply image styles). + $this->moduleHandler->alter('og_image_uri', $image_uri); + + if (UrlHelper::isExternal($image_uri)) { + return $image_uri; + } + + return $this->fileUrlGenerator->generateAbsoluteString($image_uri); + } + + /** + * Gets sorted list of image builders. + * + * @return \Drupal\helfi_platform_config\Token\OGImageBuilderInterface[] + * Image builders sorted according to priority. + */ + private function getBuilders() : array { + if (empty($this->sortedBuilders)) { + ksort($this->builders); + $this->sortedBuilders = array_merge(...$this->builders); + } + + return $this->sortedBuilders; + } + +} diff --git a/tests/src/Unit/OGImageManagerTest.php b/tests/src/Unit/OGImageManagerTest.php new file mode 100644 index 000000000..30119e67b --- /dev/null +++ b/tests/src/Unit/OGImageManagerTest.php @@ -0,0 +1,99 @@ +getSut(); + $entity = $this->prophesize(EntityInterface::class)->reveal(); + + // First builder does not apply. + $sut->add($this->createImageBuilderMock('https://1', FALSE)->reveal()); + $this->assertEquals(NULL, $sut->buildUrl($entity)); + + // Second builder applies but returns NULL. + $sut->add($this->createImageBuilderMock(NULL)->reveal()); + $this->assertEquals(NULL, $sut->buildUrl($entity)); + + // Third builder applies, priority is lower. + $sut->add($this->createImageBuilderMock('https://3')->reveal(), -10); + $this->assertEquals('https://3', $sut->buildUrl($entity)); + + // Builder with the lowers priority gets overwritten by '3'. + $builder4 = $this->createImageBuilderMock('https://4'); + $sut->add($builder4->reveal(), -100); + $this->assertEquals('https://3', $sut->buildUrl($entity)); + $builder4->buildUri(Argument::any())->shouldHaveBeenCalled(); + } + + /** + * Gets service under test. + * + * @param \Drupal\Core\File\FileUrlGeneratorInterface|null $fileUrlGenerator + * File url generator mock. + * + * @returns \Drupal\helfi_platform_config\Token\OGImageManager + * The open graph image manager. + */ + private function getSut(?FileUrlGeneratorInterface $fileUrlGenerator = NULL) : OGImageManager { + $moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + + if (!$fileUrlGenerator) { + $prophecy = $this->prophesize(FileUrlGeneratorInterface::class); + $prophecy->generateAbsoluteString(Argument::any())->willReturnArgument(0); + $fileUrlGenerator = $prophecy->reveal(); + } + + return new OGImageManager( + $moduleHandler->reveal(), + $fileUrlGenerator, + ); + } + + /** + * Creates mock image builder. + * + * @param string|null $url + * Return value for buildUrl(). + * @param bool $applies + * Return value for applies(). + * + * @return \Drupal\helfi_platform_config\Token\OGImageBuilderInterface|\Prophecy\Prophecy\ObjectProphecy + * Builder mock. + */ + private function createImageBuilderMock(?string $url, bool $applies = TRUE) : OGImageBuilderInterface|ObjectProphecy { + $builder = $this->prophesize(OGImageBuilderInterface::class); + $builder->applies(Argument::any())->willReturn($applies); + $builder->buildUri(Argument::any())->willReturn($url); + return $builder; + } + +}