Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.22% covered (warning)
77.22%
261 / 338
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Recipes
77.91% covered (warning)
77.91%
261 / 335
40.00% covered (danger)
40.00%
6 / 15
165.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 kses_tags
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 action_init
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 add_scripts
94.74% covered (success)
94.74%
36 / 38
0.00% covered (danger)
0.00%
0 / 1
10.01
 recipe_shortcode
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 recipe_shortcode_html
73.33% covered (warning)
73.33%
55 / 75
0.00% covered (danger)
0.00%
0 / 1
34.92
 recipe_image_shortcode
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 recipe_notes_shortcode
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
2.00
 recipe_ingredients_shortcode
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 recipe_nutrition_shortcode
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 output_list_content
71.15% covered (warning)
71.15%
37 / 52
0.00% covered (danger)
0.00%
0 / 1
29.60
 recipe_directions_shortcode
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 output_time
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
5.13
 output_image_html
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
7
 themecolor_styles
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3use Automattic\Jetpack\Assets;
4
5if ( ! defined( 'ABSPATH' ) ) {
6    exit( 0 );
7}
8
9/**
10 * Embed recipe 'cards' in post, with basic styling and print functionality
11 *
12 * To Do
13 * - defaults settings
14 * - basic styles/themecolor styles
15 * - validation/sanitization
16 * - print styles
17 *
18 * @package automattic/jetpack
19 */
20
21/**
22 * Register and display Recipes in posts.
23 *
24 * @phan-constructor-used-for-side-effects
25 */
26class Jetpack_Recipes {
27
28    /**
29     * Have scripts and styles been enqueued already.
30     *
31     * @var bool
32     */
33    private $scripts_and_style_included = false;
34
35    /**
36     * Constructor
37     */
38    public function __construct() {
39        add_action( 'init', array( $this, 'action_init' ) );
40    }
41
42    /**
43     * Returns KSES tags with Schema-specific attributes.
44     *
45     * @since 8.0.0
46     *
47     * @return array Array to be used by KSES.
48     */
49    private static function kses_tags() {
50        $allowedtags = wp_kses_allowed_html( 'post' );
51        // Create an array of all the tags we'd like to add the itemprop attribute to.
52        $tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time', 'span' );
53        foreach ( $tags as $tag ) {
54            if ( ! isset( $allowedtags[ $tag ] ) ) {
55                $allowedtags[ $tag ] = array();
56            }
57            $allowedtags[ $tag ]['class']    = array();
58            $allowedtags[ $tag ]['itemprop'] = array();
59            $allowedtags[ $tag ]['datetime'] = array();
60        }
61
62        // Allow the handler <a on=""> in AMP.
63        $allowedtags['a']['on'] = array();
64
65        // Allow itemscope and itemtype for divs.
66        if ( ! isset( $allowedtags['div'] ) ) {
67            $allowedtags['div'] = array();
68        }
69        $allowedtags['div']['class']     = array();
70        $allowedtags['div']['itemscope'] = array();
71        $allowedtags['div']['itemtype']  = array();
72        return $allowedtags;
73    }
74
75    /**
76     * Register our shortcode and enqueue necessary files.
77     */
78    public function action_init() {
79        // Enqueue styles if [recipe] exists.
80        add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
81
82        // Render [recipe], along with other shortcodes that can be nested within.
83        add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
84        add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
85        add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
86        add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
87        add_shortcode( 'recipe-nutrition', array( $this, 'recipe_nutrition_shortcode' ) );
88        add_shortcode( 'recipe-image', array( $this, 'recipe_image_shortcode' ) );
89    }
90
91    /**
92     * Enqueue scripts and styles
93     */
94    public function add_scripts() {
95        if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
96            return;
97        }
98
99        foreach ( $GLOBALS['posts'] as $p ) {
100            if ( isset( $p->post_content ) && has_shortcode( $p->post_content, 'recipe' ) ) {
101                $this->scripts_and_style_included = true;
102                break;
103            }
104        }
105
106        if ( ! $this->scripts_and_style_included ) {
107            return;
108        }
109
110        wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
111        wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
112
113        // add $themecolors-defined styles.
114        wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
115
116        if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
117            return;
118        }
119
120        wp_register_script(
121            'jetpack-shortcode-deps',
122            plugins_url( '_inc/build/shortcodes/js/dependencies.min.js', JETPACK__PLUGIN_FILE ),
123            array( 'jquery' ),
124            '20250905',
125            true
126        );
127
128        wp_enqueue_script(
129            'jetpack-recipes-js',
130            Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
131            array( 'jquery', 'jetpack-shortcode-deps' ),
132            '20131230',
133            false
134        );
135
136        $title_var     = wp_title( '|', false, 'right' );
137        $rtl           = is_rtl() ? '-rtl' : '';
138        $print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
139
140        wp_localize_script(
141            'jetpack-recipes-js',
142            'jetpack_recipes_vars',
143            array(
144                'pageTitle' => $title_var,
145                'loadCSS'   => $print_css_var,
146            )
147        );
148    }
149
150    /**
151     * Our [recipe] shortcode.
152     * Prints recipe data styled to look good on *any* theme.
153     *
154     * @param array  $atts    Array of shortcode attributes.
155     * @param string $content Post content.
156     *
157     * @return string HTML for recipe shortcode.
158     */
159    public static function recipe_shortcode( $atts, $content = '' ) {
160        $atts = shortcode_atts(
161            array(
162                'title'       => '', // string.
163                'servings'    => '', // intval.
164                'time'        => '', // strtotime-compatible time description.
165                'difficulty'  => '', // string.
166                'print'       => '', // URL for external print version.
167                'source'      => '', // string.
168                'sourceurl'   => '', // URL string. Only used if source set.
169                'image'       => '', // URL or attachment ID.
170                'description' => '', // string.
171                'cooktime'    => '', // strtotime-compatible time description.
172                'preptime'    => '', // strtotime-compatible time description.
173                'rating'      => '', // string.
174            ),
175            $atts,
176            'recipe'
177        );
178
179        return self::recipe_shortcode_html( $atts, $content );
180    }
181
182    /**
183     * The recipe output
184     *
185     * @param array  $atts    Array of shortcode attributes.
186     * @param string $content Post content.
187     *
188     * @return string HTML output
189     */
190    private static function recipe_shortcode_html( $atts, $content = '' ) {
191
192        $html = '<div class="hrecipe h-recipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
193
194        // Print the recipe title if exists.
195        if ( '' !== $atts['title'] ) {
196            $html .= '<h3 class="p-name jetpack-recipe-title fn" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
197        }
198
199        // Print the recipe meta if exists.
200        if (
201            '' !== $atts['servings']
202            || '' !== $atts['time']
203            || '' !== $atts['difficulty']
204            || '' !== $atts['print']
205            || '' !== $atts['preptime']
206            || '' !== $atts['cooktime']
207            || '' !== $atts['rating']
208        ) {
209            $html .= '<ul class="jetpack-recipe-meta">';
210
211            if ( '' !== $atts['servings'] ) {
212                $html .= sprintf(
213                    '<li class="jetpack-recipe-servings p-yield yield" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
214                    esc_html_x( 'Servings', 'recipe', 'jetpack' ),
215                    esc_html( $atts['servings'] )
216                );
217            }
218
219            $time_types = array( 'preptime', 'cooktime', 'time' );
220            foreach ( $time_types as $time_type ) {
221                if ( '' === $atts[ $time_type ] ) {
222                    continue;
223                }
224                $html .= self::output_time( $atts[ $time_type ], $time_type );
225            }
226
227            if ( '' !== $atts['difficulty'] ) {
228                $html .= sprintf(
229                    '<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
230                    esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
231                    esc_html( $atts['difficulty'] )
232                );
233            }
234
235            if ( '' !== $atts['rating'] ) {
236                $html .= sprintf(
237                    '<li class="jetpack-recipe-rating">
238                        <strong>%1$s: </strong>
239                        <span itemprop="contentRating">%2$s</span>
240                    </li>',
241                    esc_html_x( 'Rating', 'recipe', 'jetpack' ),
242                    esc_html( $atts['rating'] )
243                );
244            }
245
246            if ( '' !== $atts['source'] ) {
247                $html .= sprintf(
248                    '<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
249                    esc_html_x( 'Source', 'recipe', 'jetpack' )
250                );
251
252                if ( '' !== $atts['sourceurl'] ) :
253                    // Show the link if we have one.
254                    $html .= sprintf(
255                        '<a href="%2$s">%1$s</a>',
256                        esc_html( $atts['source'] ),
257                        esc_url( $atts['sourceurl'] )
258                    );
259                else :
260                    // Skip the link.
261                    $html .= sprintf(
262                        '%1$s',
263                        esc_html( $atts['source'] )
264                    );
265                endif;
266
267                $html .= '</li>';
268            }
269
270            if ( 'false' !== $atts['print'] ) {
271                $is_amp       = class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
272                $print_action = $is_amp ? 'on="tap:AMP.print"' : '';
273                $print_text   = $is_amp ? esc_html__( 'Print page', 'jetpack' ) : esc_html_x( 'Print', 'recipe', 'jetpack' );
274                $html        .= sprintf(
275                    '<li class="jetpack-recipe-print"><a href="#" %1$s>%2$s</a></li>',
276                    $print_action,
277                    $print_text
278                );
279            }
280
281            $html .= '</ul>';
282        }
283
284        // Output the image if we have one and it's not shown elsewhere.
285        if ( '' !== $atts['image'] ) {
286            if ( ! has_shortcode( $content, 'recipe-image' ) ) {
287                $html .= self::output_image_html( $atts['image'] );
288            }
289        }
290
291        // Output the description, if we have one.
292        if ( '' !== $atts['description'] ) {
293            $html .= sprintf(
294                '<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
295                esc_html( $atts['description'] )
296            );
297        }
298
299        // Print content between codes.
300        $html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
301
302        // Close it up.
303        $html .= '</div>';
304
305        // If there is a recipe within a recipe, remove the shortcode.
306        if ( has_shortcode( $html, 'recipe' ) ) {
307            remove_shortcode( 'recipe' );
308        }
309
310        // Sanitize html.
311        $html = wp_kses( $html, self::kses_tags() );
312
313        // Return the HTML block.
314        return $html;
315    }
316
317    /**
318     * Our [recipe-image] shortcode.
319     * Controls placement of image in recipe.
320     *
321     * @param array $atts Array of shortcode attributes.
322     *
323     * @return string HTML for recipe notes shortcode.
324     */
325    public static function recipe_image_shortcode( $atts ) {
326        $atts = shortcode_atts(
327            array(
328                'image' => '', // string.
329                0       => '', // string.
330            ),
331            $atts,
332            'recipe-image'
333        );
334        $src  = $atts['image'];
335        if ( ! empty( $atts[0] ) ) {
336            $src = $atts[0];
337        }
338        return self::output_image_html( $src );
339    }
340
341    /**
342     * Our [recipe-notes] shortcode.
343     * Outputs ingredients, styled in a div.
344     *
345     * @param array  $atts    Array of shortcode attributes.
346     * @param string $content Post content.
347     *
348     * @return string HTML for recipe notes shortcode.
349     */
350    public static function recipe_notes_shortcode( $atts, $content = '' ) {
351        $atts = shortcode_atts(
352            array(
353                'title' => '', // string.
354            ),
355            $atts,
356            'recipe-notes'
357        );
358
359        $html = '';
360
361        // Print a title if one exists.
362        if ( '' !== $atts['title'] ) {
363            $html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
364        }
365
366        $html .= '<div class="jetpack-recipe-notes">';
367
368        // Format content using list functionality, if desired.
369        $html .= self::output_list_content( $content, 'notes' );
370
371        $html .= '</div>';
372
373        // Sanitize html.
374        $html = wp_kses( $html, self::kses_tags() );
375
376        // Return the HTML block.
377        return $html;
378    }
379
380    /**
381     * Our [recipe-ingredients] shortcode.
382     * Outputs notes, styled in a div.
383     *
384     * @param array  $atts    Array of shortcode attributes.
385     * @param string $content Post content.
386     *
387     * @return string HTML for recipe ingredients shortcode.
388     */
389    public static function recipe_ingredients_shortcode( $atts, $content = '' ) {
390        $atts = shortcode_atts(
391            array(
392                'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
393            ),
394            $atts,
395            'recipe-ingredients'
396        );
397
398        $html = '<div class="jetpack-recipe-ingredients">';
399
400        // Print a title unless the user has opted to exclude it.
401        if ( 'false' !== $atts['title'] ) {
402            $html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
403        }
404
405        // Format content using list functionality.
406        $html .= self::output_list_content( $content, 'ingredients' );
407
408        $html .= '</div>';
409
410        // Sanitize html.
411        $html = wp_kses( $html, self::kses_tags() );
412
413        // Return the HTML block.
414        return $html;
415    }
416
417    /**
418     * Our [recipe-nutrition] shortcode.
419     * Outputs notes, styled in a div.
420     *
421     * @param array  $atts    Array of shortcode attributes.
422     * @param string $content Post content.
423     *
424     * @return string HTML for recipe nutrition shortcode.
425     */
426    public static function recipe_nutrition_shortcode( $atts, $content = '' ) {
427        $atts = shortcode_atts(
428            array(
429                'title' => esc_html_x( 'Nutrition', 'recipe', 'jetpack' ), // string.
430            ),
431            $atts,
432            'recipe-nutrition'
433        );
434
435        $html = '<div class="jetpack-recipe-nutrition p-nutrition nutrition">';
436
437        // Print a title unless the user has opted to exclude it.
438        if ( 'false' !== $atts['title'] ) {
439            $html .= '<h4 class="jetpack-recipe-nutrition-title">' . esc_html( $atts['title'] ) . '</h4>';
440        }
441
442        // Format content using list functionality.
443        $html .= self::output_list_content( $content, 'nutrition' );
444
445        $html .= '</div>';
446
447        // Sanitize html.
448        $html = wp_kses( $html, self::kses_tags() );
449
450        // Return the HTML block.
451        return $html;
452    }
453
454    /**
455     * Reusable function to check for shortened formatting.
456     * Basically, users can create lists with the following shorthand:
457     * - item one
458     * - item two
459     * - item three
460     * And we'll magically convert it to a list. This has the added benefit
461     * of including itemprops for the recipe schema.
462     *
463     * @param string $content HTML content.
464     * @param string $type    Type of list.
465     *
466     * @return string content formatted as a list item
467     */
468    private static function output_list_content( $content, $type ) {
469        $html = '';
470
471        switch ( $type ) {
472            case 'directions':
473                $list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
474                $itemprop              = ' itemprop="recipeInstructions"';
475                $listtype              = 'ol';
476                break;
477            case 'ingredients':
478                $list_item_replacement = '<li class="jetpack-recipe-ingredient p-ingredient ingredient" itemprop="recipeIngredient">${1}</li>';
479                $itemprop              = '';
480                $listtype              = 'ul';
481                break;
482            case 'nutrition':
483                $list_item_replacement = '<li class="jetpack-recipe-nutrition">${1}</li>';
484                $itemprop              = ' itemprop="nutrition"';
485                $listtype              = 'ul';
486                break;
487            case 'nutrition':
488                $list_item_replacement = '<li class="jetpack-recipe-nutrition nutrition">${1}</li>';
489                $itemprop              = ' itemprop="nutrition"';
490                $listtype              = 'ul';
491                break;
492            default:
493                $list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
494                $itemprop              = '';
495                $listtype              = 'ul';
496        }
497
498        // Check to see if the user is trying to use shortened formatting.
499        if (
500            str_contains( $content, '&#8211;' ) ||
501            str_contains( $content, '&#8212;' ) ||
502            str_contains( $content, '-' ) ||
503            str_contains( $content, '*' ) ||
504            str_contains( $content, '#' ) ||
505            str_contains( $content, '–' ) || // ndash.
506            str_contains( $content, '—' ) || // mdash.
507            preg_match( '/\d+\.\s/', $content )
508        ) {
509            // Remove breaks and extra whitespace.
510            $content = str_replace( "<br />\n", "\n", $content );
511            $content = trim( $content );
512
513            $ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
514            $ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
515
516            preg_match_all( $ul_pattern, $content, $ul_matches );
517            preg_match_all( $ol_pattern, $content, $ol_matches );
518
519            if ( ( is_countable( $ul_matches[0] ) && count( $ul_matches[0] ) > 0 ) || ( is_countable( $ol_matches[0] ) && count( $ol_matches[0] ) > 0 ) ) {
520
521                if ( is_countable( $ol_matches[0] ) && count( $ol_matches[0] ) > 0 ) {
522                    $listtype          = 'ol';
523                    $list_item_pattern = $ol_pattern;
524                } else {
525                    $listtype          = 'ul';
526                    $list_item_pattern = $ul_pattern;
527                }
528                $html .= '<' . $listtype . $itemprop . '>';
529                $html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
530                $html .= '</' . $listtype . '>';
531
532                // Strip out any empty <p> tags and stray </p> tags, because those are just silly.
533                $empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
534                $html            = preg_replace( $empty_p_pattern, '', $html );
535            } else {
536                $html .= do_shortcode( $content );
537            }
538        } else {
539            $html .= do_shortcode( $content );
540        }
541
542        // Return our formatted content.
543        return $html;
544    }
545
546    /**
547     * Our [recipe-directions] shortcode.
548     * Outputs directions, styled in a div.
549     *
550     * @param array  $atts    Array of shortcode attributes.
551     * @param string $content Post content.
552     *
553     * @return string HTML for recipe directions shortcode.
554     */
555    public static function recipe_directions_shortcode( $atts, $content = '' ) {
556        $atts = shortcode_atts(
557            array(
558                'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
559            ),
560            $atts,
561            'recipe-directions'
562        );
563
564        $html = '<div class="jetpack-recipe-directions e-instructions">';
565
566        // Print a title unless the user has specified to exclude it.
567        if ( 'false' !== $atts['title'] ) {
568            $html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
569        }
570
571        // Format content using list functionality.
572        $html .= self::output_list_content( $content, 'directions' );
573
574        $html .= '</div>';
575
576        // Sanitize html.
577        $html = wp_kses( $html, self::kses_tags() );
578
579        // Return the HTML block.
580        return $html;
581    }
582
583    /**
584     * Outputs time meta tag.
585     *
586     * @param string $time_str  Raw time to output.
587     * @param string $time_type Type of time to show.
588     *
589     * @return string HTML for recipe time meta.
590     */
591    private static function output_time( $time_str, $time_type ) {
592        // Get a time that's supported by Schema.org.
593        $duration = WPCOM_JSON_API_Date::format_duration( $time_str );
594        // If no duration can be calculated, let's output what the user provided.
595        if ( ! $duration ) {
596            $duration = $time_str;
597        }
598
599        switch ( $time_type ) {
600            case 'cooktime':
601                $title    = _x( 'Cook Time', 'recipe', 'jetpack' );
602                $itemprop = 'cookTime';
603                break;
604            case 'preptime':
605                $title    = _x( 'Prep Time', 'recipe', 'jetpack' );
606                $itemprop = 'prepTime';
607                break;
608            default:
609                $title    = _x( 'Time', 'recipe', 'jetpack' );
610                $itemprop = 'totalTime';
611                break;
612        }
613
614        return sprintf(
615            '<li class="jetpack-recipe-%3$s">
616                <time itemprop="%4$s" datetime="%5$s"><strong>%1$s:</strong> <span class="%3$s">%2$s</span></time>
617            </li>',
618            esc_html( $title ),
619            esc_html( $time_str ),
620            esc_attr( $time_type ),
621            esc_attr( $itemprop ),
622            esc_attr( $duration )
623        );
624    }
625
626    /**
627     * Outputs image tag for recipe.
628     *
629     * @param string $src The image source.
630     *
631     * @return string
632     */
633    private static function output_image_html( $src ) {
634        // Exit if there is no provided source.
635        if ( ! $src ) {
636            return '';
637        }
638
639        $image_attrs = array(
640            'class'    => 'jetpack-recipe-image u-photo photo',
641            'itemprop' => 'image',
642        );
643
644        if ( wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' ) ) {
645            $image_attrs['loading'] = 'lazy';
646        }
647
648        // If it's numeric, this may be an attachment.
649        if ( is_numeric( $src ) ) {
650            return wp_get_attachment_image(
651                $src,
652                'full',
653                false,
654                $image_attrs
655            );
656        }
657
658        // Check if it's an absolute or relative URL, and return if not.
659        if (
660            ! str_starts_with( $src, '/' )
661            && false === filter_var( $src, FILTER_VALIDATE_URL )
662        ) {
663            return '';
664        }
665
666        $image_attrs_markup = '';
667        foreach ( $image_attrs as $name => $value ) {
668            $image_attrs_markup .= sprintf(
669                ' %1$s="%2$s"',
670                esc_attr( $name ),
671                esc_attr( $value )
672            );
673        }
674
675        return sprintf(
676            '<img%1$s src="%2$s" />',
677            $image_attrs_markup,
678            esc_url( $src )
679        );
680    }
681
682    /**
683     * Use $themecolors array to style the Recipes shortcode
684     *
685     * @print style block
686     * @return string $style
687     */
688    public function themecolor_styles() {
689        global $themecolors;
690        $style = '';
691
692        if ( isset( $themecolors['border'] ) ) {
693            $style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
694        }
695        if ( isset( $themecolors['link'] ) ) {
696            $style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
697        }
698
699        return $style;
700    }
701}
702
703new Jetpack_Recipes();