Completed
Push — master-stable ( 53f101...a82972 )
by
unknown
86:26 queued 76:28
created

Jetpack_Recipes::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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