Completed
Push — add/mailchimp-groups-merge-fie... ( c88508...48f203 )
by
unknown
07:48 queued 01:05
created

Jetpack_Recipes::add_recipes_kses_rules()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 2
dl 0
loc 30
rs 9.44
c 0
b 0
f 0
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 View Code Duplication
		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 View Code Duplication
			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 View Code Duplication
			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 View Code Duplication
	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 View Code Duplication
	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';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
420
				break;
421
			case 'ingredients':
422
				$list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
423
				$itemprop              = '';
424
				$listtype              = 'ul';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
425
				break;
426
			default:
427
				$list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
428
				$itemprop              = '';
429
				$listtype              = 'ul';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 View Code Duplication
	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