PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.7
Jetpack – WP Security, Backup, Speed, & Growth v7.7
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 in Jetpack – WP Security, Backup, Speed, & Growth 7.7, at modules/shortcodes/recipe.php

538 lines 14.6 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 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 add_filter( 'wp_kses_allowed_html', array( $this, 'add_recipes_kses_rules' ), 10, 2 );
36 }
37
38 /**
39 * Add Schema-specific attributes to our allowed tags in wp_kses,
40 * so we can have better Schema.org compliance.
41 *
42 * @param array $allowedtags Array of allowed HTML tags in recipes.
43 * @param array $context Context to judge allowed tags by.
44 */
45 public function add_recipes_kses_rules( $allowedtags, $context ) {
46 if ( in_array( $context, array( '', 'post', 'data' ) ) ) : // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
47 // Create an array of all the tags we'd like to add the itemprop attribute to.
48 $tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time' );
49 foreach ( $tags as $tag ) {
50 $allowedtags = $this->add_kses_rule(
51 $allowedtags,
52 $tag,
53 array(
54 'class' => array(),
55 'itemprop' => array(),
56 'datetime' => array(),
57 )
58 );
59 }
60
61 // Allow itemscope and itemtype for divs.
62 $allowedtags = $this->add_kses_rule(
63 $allowedtags,
64 'div',
65 array(
66 'class' => array(),
67 'itemscope' => array(),
68 'itemtype' => array(),
69 )
70 );
71 endif;
72
73 return $allowedtags;
74 }
75
76 /**
77 * Function to add a new property rule to our kses array.
78 * Used by add_recipe_kses_rules() above.
79 *
80 * @param array $all_tags Array of allowed HTML tags in recipes.
81 * @param string $tag New HTML tag to add to the array of allowed HTML.
82 * @param array $rules Array of allowed attributes for that HTML tag.
83 */
84 private function add_kses_rule( $all_tags, $tag, $rules ) {
85
86 // If the tag doesn't already exist, add it.
87 if ( ! isset( $all_tags[ $tag ] ) ) {
88 $all_tags[ $tag ] = array();
89 }
90
91 // Merge the new tags with existing tags.
92 $all_tags[ $tag ] = array_merge( $all_tags[ $tag ], $rules );
93
94 return $all_tags;
95 }
96
97 /**
98 * Register our shortcode and enqueue necessary files.
99 */
100 public function action_init() {
101 // Enqueue styles if [recipe] exists.
102 add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
103
104 // Render [recipe], along with other shortcodes that can be nested within.
105 add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
106 add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
107 add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
108 add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
109 }
110
111 /**
112 * Enqueue scripts and styles
113 */
114 public function add_scripts() {
115 if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
116 return;
117 }
118
119 foreach ( $GLOBALS['posts'] as $p ) {
120 if ( has_shortcode( $p->post_content, 'recipe' ) ) {
121 $this->scripts_and_style_included = true;
122 break;
123 }
124 }
125
126 if ( ! $this->scripts_and_style_included ) {
127 return;
128 }
129
130 wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
131 wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
132
133 // add $themecolors-defined styles.
134 wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
135 wp_enqueue_script(
136 'jetpack-recipes-printthis',
137 Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
138 array( 'jquery' ),
139 '20170202',
140 false
141 );
142
143 wp_enqueue_script(
144 'jetpack-recipes-js',
145 Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
146 array( 'jquery', 'jetpack-recipes-printthis' ),
147 '20131230',
148 false
149 );
150
151 $title_var = wp_title( '|', false, 'right' );
152 $rtl = is_rtl() ? '-rtl' : '';
153 $print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
154
155 wp_localize_script(
156 'jetpack-recipes-js',
157 'jetpack_recipes_vars',
158 array(
159 'pageTitle' => $title_var,
160 'loadCSS' => $print_css_var,
161 )
162 );
163 }
164
165 /**
166 * Our [recipe] shortcode.
167 * Prints recipe data styled to look good on *any* theme.
168 *
169 * @param array $atts Array of shortcode attributes.
170 * @param string $content Post content.
171 *
172 * @return string HTML for recipe shortcode.
173 */
174 public static function recipe_shortcode( $atts, $content = '' ) {
175 $atts = shortcode_atts(
176 array(
177 'title' => '', // string.
178 'servings' => '', // intval.
179 'time' => '', // string.
180 'difficulty' => '', // string.
181 'print' => '', // string.
182 'source' => '', // string.
183 'sourceurl' => '', // string.
184 'image' => '', // string.
185 'description' => '', // string.
186 ),
187 $atts,
188 'recipe'
189 );
190
191 return self::recipe_shortcode_html( $atts, $content );
192 }
193
194 /**
195 * The recipe output
196 *
197 * @param array $atts Array of shortcode attributes.
198 * @param string $content Post content.
199 *
200 * @return string HTML output
201 */
202 private static function recipe_shortcode_html( $atts, $content = '' ) {
203
204 $html = '<div class="hrecipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
205
206 // Print the recipe title if exists.
207 if ( '' !== $atts['title'] ) {
208 $html .= '<h3 class="jetpack-recipe-title" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
209 }
210
211 // Print the recipe meta if exists.
212 if (
213 '' !== $atts['servings']
214 || '' !== $atts['time']
215 || '' !== $atts['difficulty']
216 || '' !== $atts['print']
217 ) {
218 $html .= '<ul class="jetpack-recipe-meta">';
219
220 if ( '' !== $atts['servings'] ) {
221 $html .= sprintf(
222 '<li class="jetpack-recipe-servings" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
223 esc_html_x( 'Servings', 'recipe', 'jetpack' ),
224 esc_html( $atts['servings'] )
225 );
226 }
227
228 if ( '' !== $atts['time'] ) {
229 // Get a time that's supported by Schema.org.
230 $duration = WPCOM_JSON_API_Date::format_duration( $atts['time'] );
231 // If no duration can be calculated, let's output what the user provided.
232 if ( empty( $duration ) ) {
233 $duration = $atts['time'];
234 }
235
236 $html .= sprintf(
237 '<li class="jetpack-recipe-time">
238 <time itemprop="totalTime" datetime="%3$s"><strong>%1$s: </strong>%2$s</time>
239 </li>',
240 esc_html_x( 'Time', 'recipe', 'jetpack' ),
241 esc_html( $atts['time'] ),
242 esc_attr( $duration )
243 );
244 }
245
246 if ( '' !== $atts['difficulty'] ) {
247 $html .= sprintf(
248 '<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
249 esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
250 esc_html( $atts['difficulty'] )
251 );
252 }
253
254 if ( '' !== $atts['source'] ) {
255 $html .= sprintf(
256 '<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
257 esc_html_x( 'Source', 'recipe', 'jetpack' )
258 );
259
260 if ( '' !== $atts['sourceurl'] ) :
261 // Show the link if we have one.
262 $html .= sprintf(
263 '<a href="%2$s">%1$s</a>',
264 esc_html( $atts['source'] ),
265 esc_url( $atts['sourceurl'] )
266 );
267 else :
268 // Skip the link.
269 $html .= sprintf(
270 '%1$s',
271 esc_html( $atts['source'] )
272 );
273 endif;
274
275 $html .= '</li>';
276 }
277
278 if ( 'false' !== $atts['print'] ) {
279 $html .= sprintf(
280 '<li class="jetpack-recipe-print"><a href="#">%1$s</a></li>',
281 esc_html_x( 'Print', 'recipe', 'jetpack' )
282 );
283 }
284
285 $html .= '</ul>';
286 }
287
288 // Output the image, if we have one.
289 if ( '' !== $atts['image'] ) {
290 $html .= sprintf(
291 '<img class="jetpack-recipe-image" itemprop="image" src="%1$s" />',
292 esc_url( $atts['image'] )
293 );
294 }
295
296 // Output the description, if we have one.
297 if ( '' !== $atts['description'] ) {
298 $html .= sprintf(
299 '<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
300 esc_html( $atts['description'] )
301 );
302 }
303
304 // Print content between codes.
305 $html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
306
307 // Close it up.
308 $html .= '</div>';
309
310 // If there is a recipe within a recipe, remove the shortcode.
311 if ( has_shortcode( $html, 'recipe' ) ) {
312 remove_shortcode( 'recipe' );
313 }
314
315 // Sanitize html.
316 $html = wp_kses_post( $html );
317
318 // Return the HTML block.
319 return $html;
320 }
321
322 /**
323 * Our [recipe-notes] shortcode.
324 * Outputs ingredients, styled in a div.
325 *
326 * @param array $atts Array of shortcode attributes.
327 * @param string $content Post content.
328 *
329 * @return string HTML for recipe notes shortcode.
330 */
331 public static function recipe_notes_shortcode( $atts, $content = '' ) {
332 $atts = shortcode_atts(
333 array(
334 'title' => '', // string.
335 ),
336 $atts,
337 'recipe-notes'
338 );
339
340 $html = '';
341
342 // Print a title if one exists.
343 if ( '' !== $atts['title'] ) {
344 $html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
345 }
346
347 $html .= '<div class="jetpack-recipe-notes">';
348
349 // Format content using list functionality, if desired.
350 $html .= self::output_list_content( $content, 'notes' );
351
352 $html .= '</div>';
353
354 // Sanitize html.
355 $html = wp_kses_post( $html );
356
357 // Return the HTML block.
358 return $html;
359 }
360
361 /**
362 * Our [recipe-ingredients] shortcode.
363 * Outputs notes, styled in a div.
364 *
365 * @param array $atts Array of shortcode attributes.
366 * @param string $content Post content.
367 *
368 * @return string HTML for recipe ingredients shortcode.
369 */
370 public static function recipe_ingredients_shortcode( $atts, $content = '' ) {
371 $atts = shortcode_atts(
372 array(
373 'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
374 ),
375 $atts,
376 'recipe-ingredients'
377 );
378
379 $html = '<div class="jetpack-recipe-ingredients">';
380
381 // Print a title unless the user has opted to exclude it.
382 if ( 'false' !== $atts['title'] ) {
383 $html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
384 }
385
386 // Format content using list functionality.
387 $html .= self::output_list_content( $content, 'ingredients' );
388
389 $html .= '</div>';
390
391 // Sanitize html.
392 $html = wp_kses_post( $html );
393
394 // Return the HTML block.
395 return $html;
396 }
397
398 /**
399 * Reusable function to check for shortened formatting.
400 * Basically, users can create lists with the following shorthand:
401 * - item one
402 * - item two
403 * - item three
404 * And we'll magically convert it to a list. This has the added benefit
405 * of including itemprops for the recipe schema.
406 *
407 * @param string $content HTML content.
408 * @param string $type Type of list.
409 *
410 * @return string content formatted as a list item
411 */
412 private static function output_list_content( $content, $type ) {
413 $html = '';
414
415 switch ( $type ) {
416 case 'directions':
417 $list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
418 $itemprop = ' itemprop="recipeInstructions"';
419 $listtype = 'ol';
420 break;
421 case 'ingredients':
422 $list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
423 $itemprop = '';
424 $listtype = 'ul';
425 break;
426 default:
427 $list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
428 $itemprop = '';
429 $listtype = 'ul';
430 }
431
432 // Check to see if the user is trying to use shortened formatting.
433 if (
434 strpos( $content, '&#8211;' ) !== false ||
435 strpos( $content, '&#8212;' ) !== false ||
436 strpos( $content, '-' ) !== false ||
437 strpos( $content, '*' ) !== false ||
438 strpos( $content, '#' ) !== false ||
439 strpos( $content, '' ) !== false || // ndash.
440 strpos( $content, '' ) !== false || // mdash.
441 preg_match( '/\d+\.\s/', $content )
442 ) {
443 // Remove breaks and extra whitespace.
444 $content = str_replace( "<br />\n", "\n", $content );
445 $content = trim( $content );
446
447 $ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
448 $ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
449
450 preg_match_all( $ul_pattern, $content, $ul_matches );
451 preg_match_all( $ol_pattern, $content, $ol_matches );
452
453 if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
454
455 if ( 0 !== count( $ol_matches[0] ) ) {
456 $listtype = 'ol';
457 $list_item_pattern = $ol_pattern;
458 } else {
459 $listtype = 'ul';
460 $list_item_pattern = $ul_pattern;
461 }
462 $html .= '<' . $listtype . $itemprop . '>';
463 $html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
464 $html .= '</' . $listtype . '>';
465
466 // Strip out any empty <p> tags and stray </p> tags, because those are just silly.
467 $empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
468 $html = preg_replace( $empty_p_pattern, '', $html );
469 } else {
470 $html .= do_shortcode( $content );
471 }
472 } else {
473 $html .= do_shortcode( $content );
474 }
475
476 // Return our formatted content.
477 return $html;
478 }
479
480 /**
481 * Our [recipe-directions] shortcode.
482 * Outputs directions, styled in a div.
483 *
484 * @param array $atts Array of shortcode attributes.
485 * @param string $content Post content.
486 *
487 * @return string HTML for recipe directions shortcode.
488 */
489 public static function recipe_directions_shortcode( $atts, $content = '' ) {
490 $atts = shortcode_atts(
491 array(
492 'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
493 ),
494 $atts,
495 'recipe-directions'
496 );
497
498 $html = '<div class="jetpack-recipe-directions">';
499
500 // Print a title unless the user has specified to exclude it.
501 if ( 'false' !== $atts['title'] ) {
502 $html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
503 }
504
505 // Format content using list functionality.
506 $html .= self::output_list_content( $content, 'directions' );
507
508 $html .= '</div>';
509
510 // Sanitize html.
511 $html = wp_kses_post( $html );
512
513 // Return the HTML block.
514 return $html;
515 }
516
517 /**
518 * Use $themecolors array to style the Recipes shortcode
519 *
520 * @print style block
521 * @return string $style
522 */
523 public function themecolor_styles() {
524 global $themecolors;
525 $style = '';
526
527 if ( isset( $themecolors ) ) {
528 $style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
529 $style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
530 }
531
532 return $style;
533 }
534
535 }
536
537 new Jetpack_Recipes();
538