diff --git a/json_ld_schema.module b/json_ld_schema.module index 8d3a23d9b3672c0d0d7e02a20c9add7e1159a49e..d23f7c0ad47e88d33bc9628a3a3c3c0ec18460ab 100644 --- a/json_ld_schema.module +++ b/json_ld_schema.module @@ -63,7 +63,7 @@ function json_ld_schema_entity_view(array &$build, EntityInterface $entity, Enti '#tag' => 'script', '#attributes' => ['type' => 'application/ld+json'], '#value' => json_encode($json_ld_entity->getData($entity, $view_mode) - ->toArray(), JSON_UNESCAPED_UNICODE), + ->toArray(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), ], sprintf('json_ld_%s_%s_%s_%s', $entity->getEntityTypeId(), $entity->id(), $view_mode, $definition['id']), ]; diff --git a/src/Element/JsonLdSource.php b/src/Element/JsonLdSource.php index c93762e655f7852c2cb02ad901d8eda10bae0a06..c50d4fff64fc62992d5a1c0822a4364df735e510 100644 --- a/src/Element/JsonLdSource.php +++ b/src/Element/JsonLdSource.php @@ -35,7 +35,7 @@ class JsonLdSource extends RenderElement { // that it is cached by render cache. $source_manager = \Drupal::service('plugin.manager.json_ld_schema.source'); $plugin = $source_manager->createInstance($element['#plugin_id']); - $element['#value'] = json_encode($plugin->getData()->toArray(), JSON_UNESCAPED_UNICODE); + $element['#value'] = json_encode($plugin->getData()->toArray(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); return $element; } diff --git a/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php b/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php index 9abc8f06150066b1bc4b76e4f08bc4f6e6020ead..5b3b918b4e432e45d24cb61c19c3831025a08c13 100644 --- a/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php +++ b/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php @@ -11,11 +11,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base class for JSON LD sources that should appear on entity pages. - * - * @deprecated - * Use the JsonLdEntity plugin type instead of this base class. This used to - * be a thing until JsonLdEntity, but now it only exists as a good test case - * for the render cache integration of JsonLdSource. */ abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements ContainerFactoryPluginInterface { @@ -49,7 +44,7 @@ abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements Contai $configuration, $plugin_id, $plugin_definition, - $container->get('current_route_match') + $container->get('current_route_match'), ); } @@ -60,7 +55,7 @@ abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements Contai * An entity for FALSE if none was found. */ private function getRawEntityFromRoute() { - list($route_type, $entity_type_id, $entity_route_type) = array_pad(explode('.', $this->currentRouteMatch->getRouteName()), 3, NULL); + [$route_type, $entity_type_id, $entity_route_type] = array_pad(explode('.', $this->currentRouteMatch->getRouteName()), 3, NULL); if ($route_type !== 'entity' || $entity_route_type !== 'canonical') { return FALSE; } diff --git a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php new file mode 100644 index 0000000000000000000000000000000000000000..3e7d77e07a4bae0d123ad72ef719a46ddf34f036 --- /dev/null +++ b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\json_ld_schema_test_sources\Plugin\JsonLdEntity; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\json_ld_schema\Entity\JsonLdEntityBase; +use Spatie\SchemaOrg\FAQPage; +use Spatie\SchemaOrg\Schema; + +/** + * Test markup is properly escaped. + * + * @JsonLdEntity( + * label = "FAQ Full Test Entity", + * id = "faq_full_test", + * ) + */ +class FaqFullTestEntity extends JsonLdEntityBase { + + /** + * {@inheritdoc} + */ + public function isApplicable(EntityInterface $entity, $view_mode) { + return $entity->getEntityTypeId() === 'node' && $entity->bundle() === 'faq' && $view_mode === 'full'; + } + + /** + * {@inheritdoc} + */ + public function getData(EntityInterface $entity, $view_mode): FAQPage { + return Schema::FAQPage() + ->mainEntity([ + Schema::Question() + ->name("question") + ->acceptedAnswer( + Schema::Answer()->text("<ul><li><a href='http://example.org'>answer!</a></li></ul>"), + ), + ]); + } + +} diff --git a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php index 995d6a15dc84ab43aec9ab25c64f0b313c6d43e6..3c3f874129249ea23ae85676b50b8148463fabaa 100644 --- a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php +++ b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php @@ -22,7 +22,7 @@ class NodeFullTestEntity extends JsonLdEntityBase { * {@inheritdoc} */ public function isApplicable(EntityInterface $entity, $view_mode) { - return $entity->getEntityTypeId() === 'node' && $view_mode === 'full'; + return $entity->getEntityTypeId() === 'node' && $entity->bundle() === 'example' && $view_mode === 'full'; } /** diff --git a/tests/src/Functional/JsonLdEntityTest.php b/tests/src/Functional/JsonLdEntityTest.php index 9ce4384e7feac515b63372fdd3f82a8fd89a53f5..edcd0296f2537dbe1e4ee86ba9247f7950859249 100644 --- a/tests/src/Functional/JsonLdEntityTest.php +++ b/tests/src/Functional/JsonLdEntityTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\json_ld_schema\Functional; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\json_ld_schema\Traits\JsonLdTestTrait; /** * Test the rendering of JSON LD scripts. @@ -13,6 +14,8 @@ use Drupal\Tests\BrowserTestBase; */ class JsonLdEntityTest extends BrowserTestBase { + use JsonLdTestTrait; + /** * {@inheritdoc} */ @@ -29,7 +32,7 @@ class JsonLdEntityTest extends BrowserTestBase { /** * Test the rendering of JSON LD and the integration with render cache. */ - public function testJsonLdEntity() { + public function testJsonLdEntity(): void { NodeType::create([ 'type' => 'example', 'label' => 'Example', @@ -41,8 +44,9 @@ class JsonLdEntityTest extends BrowserTestBase { ]); $node->save(); - $this->drupalGet($node->tourl()); - $this->assertStringContainsString('"@type":"Brewery","name":"Example Node"', $this->getSession()->getPage()->getHtml()); + $this->drupalGet($node->toUrl()); + $schema = $this->getSchemaValues('Brewery'); + $this->assertEquals('Example Node', $schema['name']); $this->assertBreadcrumbs(); $this->assertOrganization(); @@ -51,22 +55,42 @@ class JsonLdEntityTest extends BrowserTestBase { // Change the low rating and revisit the page to ensure "1" was render // cached. $this->setRatingLow(2); - $this->drupalGet($node->tourl()); + $this->drupalGet($node->toUrl()); $this->assertRating(1); // Since query args were added as cache context, visit the node with the // high rating flag set. - $this->drupalGet($node->tourl(), ['query' => ['star_rating' => 'high']]); + $this->drupalGet($node->toUrl(), ['query' => ['star_rating' => 'high']]); $this->assertRating(5); } + /** + * Test the rendering of JSON LD with markup is properly escaped. + */ + public function testMarkupEscaped(): void { + NodeType::create([ + 'type' => 'faq', + 'label' => 'Faq', + ])->save(); + + $node = Node::create([ + 'type' => 'faq', + 'title' => 'Example FAQ', + ]); + $node->save(); + + $this->drupalGet($node->toUrl()); + $schema = $this->getSchemaValues('FAQPage'); + $this->assertEquals("<ul><li><a href='http://example.org'>answer!</a></li></ul>", $schema['mainEntity'][0]['acceptedAnswer']['text']); + } + /** * Set the high rating. * * @param int $rating * The low rating. */ - protected function setRatingHigh($rating) { + protected function setRatingHigh(int $rating): void { \Drupal::state()->set('json_ld_entity_test_rating_high', $rating); } @@ -76,7 +100,7 @@ class JsonLdEntityTest extends BrowserTestBase { * @param int $rating * The low rating. */ - protected function setRatingLow($rating) { + protected function setRatingLow(int $rating): void { \Drupal::state()->set('json_ld_entity_test_rating_low', $rating); } @@ -86,22 +110,31 @@ class JsonLdEntityTest extends BrowserTestBase { * @param int $rating * The rating. */ - protected function assertRating($rating) { - $this->assertStringContainsString('{"@type":"AggregateRating","ratingValue":' . $rating . '}', $this->getSession()->getPage()->getHtml()); + protected function assertRating(int $rating): void { + $schema = $this->getSchemaValues('Brewery'); + $this->assertEquals($rating, $schema['aggregateRating'][0]['ratingValue']); } /** * Assert the ld+json for Breadcrumbs are correct. */ - protected function assertBreadcrumbs() { - $this->assertStringContainsString('"@context":"https:\/\/schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home"', $this->getSession()->getPage()->getHtml()); + protected function assertBreadcrumbs(): void { + $schema = $this->getSchemaValues('BreadcrumbList'); + $this->assertEquals([ + ['@type' => 'ListItem', 'position' => 1, 'name' => 'Home'], + ], $schema['itemListElement']); } /** * Assert the ld+json for Organization are correct. */ - protected function assertOrganization() { - $this->assertStringContainsString('"@context":"https:\/\/schema.org","@type":"Organization","url":"http:\/\/www.example.com","logo":"http:\/\/www.example.com\/logo.jpg"', $this->getSession()->getPage()->getHtml()); + protected function assertOrganization(): void { + $schema = $this->getSchemaValues('Organization'); + $this->assertEquals([ + '@type' => 'Organization', + 'url' => 'http://www.example.com', + 'logo' => 'http://www.example.com/logo.jpg', + ], $schema); } } diff --git a/tests/src/Functional/JsonLdSourceTest.php b/tests/src/Functional/JsonLdSourceTest.php index 62d1f0349793e84368c29e97fd4ba1e77242cea5..81795f9e2474f39768891eefe96d35202c924cbc 100644 --- a/tests/src/Functional/JsonLdSourceTest.php +++ b/tests/src/Functional/JsonLdSourceTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\json_ld_schema\Functional; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\json_ld_schema\Traits\JsonLdTestTrait; /** * Test the rendering of JSON LD scripts. @@ -13,6 +14,8 @@ use Drupal\Tests\BrowserTestBase; */ class JsonLdSourceTest extends BrowserTestBase { + use JsonLdTestTrait; + /** * {@inheritdoc} */ @@ -31,10 +34,11 @@ class JsonLdSourceTest extends BrowserTestBase { */ public function testRendering() { $this->drupalGet('<front>'); - $html = $this->getSession()->getPage()->getHtml(); - $this->assertStringContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Foo"}</script>', $html); - $this->assertStringContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Bar"}</script>', $html); - $this->assertStringNotContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Baz"}</script>', $html); + $schema = $this->getSchemaValues('Thing', TRUE); + $this->assertEquals([ + ['@type' => 'Thing', 'name' => 'Bar'], + ['@type' => 'Thing', 'name' => 'Foo'], + ], $schema); } /** diff --git a/tests/src/Traits/JsonLdTestTrait.php b/tests/src/Traits/JsonLdTestTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..ccaada9f6418b26574b99c670353ed177f4f8293 --- /dev/null +++ b/tests/src/Traits/JsonLdTestTrait.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\Tests\json_ld_schema\Traits; + +use Drupal\Component\Serialization\Json; + +/** + * Helper functions. + */ +trait JsonLdTestTrait { + + /** + * Reads and decodes json schema data from the page. + */ + protected function getSchemaValues(string $type = NULL, bool $all = FALSE): array { + $schemas = $this->getSession()->getPage()->findAll('xpath', '//script[@type="application/ld+json"]'); + $return = []; + foreach ($schemas as $schema) { + $this->assertNotEmpty($schema); + $json = Json::decode($schema->getHtml()); + $this->assertNotEmpty($json); + unset($json['@context']); + if (!$type) { + if (!$all) { + return $json; + } + $return[] = $json; + } + + if ($json['@type'] === $type) { + if (!$all) { + return $json; + } + $return[] = $json; + } + } + return $return; + } + +}