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