PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 15.0
Jetpack – WP Security, Backup, Speed, & Growth v15.0
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
704 lines 19.1 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 if ( ! 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 */
26 class 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_enqueue_script(
121 'jetpack-recipes-printthis',
122 Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
123 array( 'jquery' ),
124 '20170202',
125 false
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-recipes-printthis' ),
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
703 new Jetpack_Recipes();
704