';
// Print a title unless the user has specified to exclude it.
if ( 'false' !== $atts['title'] ) {
@@ -508,12 +556,95 @@ public static function recipe_directions_shortcode( $atts, $content = '' ) {
$html .= '
';
// Sanitize html.
- $html = wp_kses_post( $html );
+ $html = wp_kses( $html, self::kses_tags() );
// Return the HTML block.
return $html;
}
+ /**
+ * Outputs time meta tag.
+ *
+ * @param string $time_str Raw time to output.
+ * @param string $time_type Type of time to show.
+ *
+ * @return string HTML for recipe time meta.
+ */
+ private static function output_time( $time_str, $time_type ) {
+ // Get a time that's supported by Schema.org.
+ $duration = WPCOM_JSON_API_Date::format_duration( $time_str );
+ // If no duration can be calculated, let's output what the user provided.
+ if ( ! $duration ) {
+ $duration = $time_str;
+ }
+
+ switch ( $time_type ) {
+ case 'cooktime':
+ $title = _x( 'Cook Time', 'recipe', 'jetpack' );
+ $itemprop = 'cookTime';
+ break;
+ case 'preptime':
+ $title = _x( 'Prep Time', 'recipe', 'jetpack' );
+ $itemprop = 'prepTime';
+ break;
+ default:
+ $title = _x( 'Time', 'recipe', 'jetpack' );
+ $itemprop = 'totalTime';
+ break;
+ }
+
+ return sprintf(
+ '
+
+
',
+ esc_html( $title ),
+ esc_html( $time_str ),
+ esc_attr( $time_type ),
+ esc_attr( $itemprop ),
+ esc_attr( $duration )
+ );
+ }
+
+ /**
+ * Outputs image tag for recipe.
+ *
+ * @param string $src The image source.
+ *
+ * @return string
+ */
+ private static function output_image_html( $src ) {
+ // Exit if there is no provided source.
+ if ( ! $src ) {
+ return '';
+ }
+
+ // If it's numeric, this may be an attachment.
+ if ( is_numeric( $src ) ) {
+ return wp_get_attachment_image(
+ $src,
+ 'full',
+ false,
+ array(
+ 'class' => 'jetpack-recipe-image u-photo photo',
+ 'itemprop' => 'image',
+ )
+ );
+ }
+
+ // Check if it's an absolute or relative URL, and return if not.
+ if (
+ 0 !== strpos( $src, '/' )
+ && false === filter_var( $src, FILTER_VALIDATE_URL )
+ ) {
+ return '';
+ }
+
+ return sprintf(
+ '',
+ esc_url( $src )
+ );
+ }
+
/**
* Use $themecolors array to style the Recipes shortcode
*
diff --git a/tests/php/modules/shortcodes/test-class.recipe.php b/tests/php/modules/shortcodes/test-class.recipe.php
new file mode 100644
index 0000000000000..af71d9e080a13
--- /dev/null
+++ b/tests/php/modules/shortcodes/test-class.recipe.php
@@ -0,0 +1,351 @@
+assertEquals( shortcode_exists( 'recipe' ), true );
+ $this->assertEquals( shortcode_exists( 'recipe-notes' ), true );
+ $this->assertEquals( shortcode_exists( 'recipe-ingredients' ), true );
+ $this->assertEquals( shortcode_exists( 'recipe-directions' ), true );
+ $this->assertEquals( shortcode_exists( 'recipe-nutrition' ), true );
+ $this->assertEquals( shortcode_exists( 'recipe-image' ), true );
+ }
+
+ /**
+ * Verify that the recipe shortcode outputs prep time in HTML.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_preptime() {
+ $content = '[recipe preptime="30 min"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe shortcode outputs cook time in HTML.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_cooktime() {
+ $content = '[recipe cooktime="2 hours 30 min"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe shortcode outputs rating in HTML.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_rating() {
+ $content = '[recipe rating="2 stars"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '2 stars', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe shortcode does not output an image with an empty source.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_empty_src() {
+ $content = '[recipe image=""]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertNotContains( 'assertNotContains( 'assertNotContains( '_make_attachment(
+ array(
+ 'file' => 'example.jpg',
+ 'url' => 'http://example.com/wp-content/uploads/example.jpg',
+ 'type' => 'image/jpeg',
+ 'error' => false,
+ )
+ );
+
+ // Get shortcode with new attachment.
+ $content = '[recipe image="' . $attachment_id . '"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe shortcode outputs an image with a src string.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_src() {
+ $content = '[recipe image="https://example.com"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe shortcode does not output an image if an empty recipe-image shortcode exists.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_location_move() {
+ $content = '[recipe image="https://example.com"][recipe-image][/recipe]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertNotContains( 'assertNotContains( 'assertNotContains( 'assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe-image shortcode outputs an image with a string parameter.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_shortcode_src_attr() {
+
+ $content = '[recipe-image image="https://example.com"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe-image shortcode does not output an image with an invalid attachment.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_shortcode_invalid_attachment() {
+ $content = '[recipe-image -100]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertNotContains( 'assertNotContains( '_make_attachment(
+ array(
+ 'file' => 'example.jpg',
+ 'url' => 'http://example.com/wp-content/uploads/example.jpg',
+ 'type' => 'image/jpeg',
+ 'error' => false,
+ )
+ );
+
+ // Get shortcode with new attachment.
+ $content = '[recipe-image ' . $attachment_id . ']';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe-image shortcode outputs an image with a valid attachment ID attribute.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_image_shortcode_attachment_attr() {
+ // Create a mock attachment.
+ $attachment_id = $this->_make_attachment(
+ array(
+ 'file' => 'example.jpg',
+ 'url' => 'http://example.com/wp-content/uploads/example.jpg',
+ 'type' => 'image/jpeg',
+ 'error' => false,
+ )
+ );
+
+ // Get shortcode with new attachment.
+ $content = '[recipe-image image="' . $attachment_id . '"]';
+
+ $shortcode_content = do_shortcode( $content );
+ $this->assertContains( '', $shortcode_content );
+ }
+
+ /**
+ * Verify that the recipe-nutrition shortcode formats a list of nutrition info.
+ *
+ * @covers ::recipe_shortcode
+ *
+ * @since 8.0.0
+ */
+ public function test_shortcodes_recipe_nutrition() {
+ $content = <<assertContains( 'itemprop="nutrition"', $shortcode_content );
+ $this->assertContains( '