Completed
Push — update/shortcode-file-constant... ( 547d07 )
by
unknown
09:22
created

Jetpack_Recipes   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 494
Duplicated Lines 17.21 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
dl 85
loc 494
rs 7.4757
c 0
b 0
f 0
wmc 53
lcom 2
cbo 2

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B add_recipes_kses_rules() 0 30 3
A add_kses_rule() 0 12 2
A action_init() 0 10 1
B add_scripts() 0 55 7
A recipe_shortcode() 0 17 1
F recipe_shortcode_html() 14 114 16
B recipe_notes_shortcode() 25 25 2
A recipe_ingredients_shortcode() 23 23 2
C output_list_content() 0 67 14
A recipe_directions_shortcode() 23 23 2
A themecolor_styles() 0 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Recipes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Recipes, and based on these observations, apply Extract Interface, too.

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
		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
		wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
115
		wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
116
117
		// add $themecolors-defined styles.
118
		wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
119
120
		wp_enqueue_script(
121
			'jetpack-recipes-printthis',
122
			Jetpack::get_file_url_for_environment(
123
				'_inc/build/shortcodes/js/recipes-printthis.min.js',
124
				JETPACK_SHORTCODES_PLUGIN_URL . 'js/recipes-printthis.js'
125
			),
126
			array( 'jquery' ),
127
			'20170202'
128
		);
129
130
		wp_enqueue_script(
131
			'jetpack-recipes-js',
132
			Jetpack::get_file_url_for_environment(
133
				'_inc/build/shortcodes/js/recipes.min.js',
134
				JETPACK_SHORTCODES_PLUGIN_URL . 'js/recipes.js'
135
			),
136
			array( 'jquery', 'jetpack-recipes-printthis' ),
137
			'20131230'
138
		);
139
140
		$title_var     = wp_title( '|', false, 'right' );
141
		$rtl           = is_rtl() ? '-rtl' : '';
142
		$print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
143
144
		wp_localize_script(
145
			'jetpack-recipes-js',
146
			'jetpack_recipes_vars',
147
			array(
148
				'pageTitle' => $title_var,
149
				'loadCSS' => $print_css_var,
150
			)
151
		);
152
	}
153
154
	/**
155
	 * Our [recipe] shortcode.
156
	 * Prints recipe data styled to look good on *any* theme.
157
	 *
158
	 * @param array  $atts    Array of shortcode attributes.
159
	 * @param string $content Post content.
160
	 *
161
	 * @return string HTML for recipe shortcode.
162
	 */
163
	static function recipe_shortcode( $atts, $content = '' ) {
164
		$atts = shortcode_atts(
165
			array(
166
				'title'       => '', // string.
167
				'servings'    => '', // intval.
168
				'time'        => '', // string.
169
				'difficulty'  => '', // string.
170
				'print'       => '', // string.
171
				'source'      => '', // string.
172
				'sourceurl'   => '', // string.
173
				'image'       => '', // string.
174
				'description' => '', // string.
175
			), $atts, 'recipe'
176
		);
177
178
		return self::recipe_shortcode_html( $atts, $content );
179
	}
180
181
	/**
182
	 * The recipe output
183
	 *
184
	 * @param array  $atts    Array of shortcode attributes.
185
	 * @param string $content Post content.
186
	 *
187
	 * @return string HTML output
188
	 */
189
	static function recipe_shortcode_html( $atts, $content = '' ) {
190
191
		$html = '<div class="hrecipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
192
193
		// Print the recipe title if exists.
194
		if ( '' !== $atts['title'] ) {
195
			$html .= '<h3 class="jetpack-recipe-title" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
196
		}
197
198
		// Print the recipe meta if exists.
199
		if ( '' !== $atts['servings'] || '' != $atts['time'] || '' != $atts['difficulty'] || '' != $atts['print'] ) {
200
			$html .= '<ul class="jetpack-recipe-meta">';
201
202 View Code Duplication
			if ( '' !== $atts['servings'] ) {
203
				$html .= sprintf(
204
					'<li class="jetpack-recipe-servings" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
205
					esc_html_x( 'Servings', 'recipe', 'jetpack' ),
206
					esc_html( $atts['servings'] )
207
				);
208
			}
209
210
			if ( '' !== $atts['time'] ) {
211
				// Get a time that's supported by Schema.org.
212
				$duration = WPCOM_JSON_API_Date::format_duration( $atts['time'] );
213
				// If no duration can be calculated, let's output what the user provided.
214
				if ( empty( $duration ) ) {
215
					$duration = $atts['time'];
216
				}
217
218
				$html .= sprintf(
219
					'<li class="jetpack-recipe-time">
220
					<time itemprop="totalTime" datetime="%3$s"><strong>%1$s: </strong>%2$s</time>
221
					</li>',
222
					esc_html_x( 'Time', 'recipe', 'jetpack' ),
223
					esc_html( $atts['time'] ),
224
					esc_attr( $duration )
225
				);
226
			}
227
228 View Code Duplication
			if ( '' !== $atts['difficulty'] ) {
229
				$html .= sprintf(
230
					'<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
231
					esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
232
					esc_html( $atts['difficulty'] )
233
				);
234
			}
235
236
			if ( '' !== $atts['source'] ) {
237
				$html .= sprintf(
238
					'<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
239
					esc_html_x( 'Source', 'recipe', 'jetpack' )
240
				);
241
242
				if ( '' !== $atts['sourceurl'] ) :
243
					// Show the link if we have one.
244
					$html .= sprintf(
245
						'<a href="%2$s">%1$s</a>',
246
						esc_html( $atts['source'] ),
247
						esc_url( $atts['sourceurl'] )
248
					);
249
				else :
250
					// Skip the link.
251
					$html .= sprintf(
252
						'%1$s',
253
						esc_html( $atts['source'] )
254
					);
255
				endif;
256
257
				$html .= '</li>';
258
			}
259
260
			if ( 'false' !== $atts['print'] ) {
261
				$html .= sprintf(
262
					'<li class="jetpack-recipe-print"><a href="#">%1$s</a></li>',
263
					esc_html_x( 'Print', 'recipe', 'jetpack' )
264
				);
265
			}
266
267
			$html .= '</ul>';
268
		} // End if().
269
270
		// Output the image, if we have one.
271
		if ( '' !== $atts['image'] ) {
272
			$html .= sprintf(
273
				'<img class="jetpack-recipe-image" itemprop="image" src="%1$s" />',
274
				esc_url( $atts['image'] )
275
			);
276
		}
277
278
		// Output the description, if we have one.
279
		if ( '' !== $atts['description'] ) {
280
			$html .= sprintf(
281
				'<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
282
				esc_html( $atts['description'] )
283
			);
284
		}
285
286
		// Print content between codes.
287
		$html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
288
289
		// Close it up.
290
		$html .= '</div>';
291
292
		// If there is a recipe within a recipe, remove the shortcode.
293
		if ( has_shortcode( $html, 'recipe' ) ) {
294
			remove_shortcode( 'recipe' );
295
		}
296
297
		// Sanitize html.
298
		$html = wp_kses_post( $html );
299
300
		// Return the HTML block.
301
		return $html;
302
	}
303
304
	/**
305
	 * Our [recipe-notes] shortcode.
306
	 * Outputs ingredients, styled in a div.
307
	 *
308
	 * @param array  $atts    Array of shortcode attributes.
309
	 * @param string $content Post content.
310
	 *
311
	 * @return string HTML for recipe notes shortcode.
312
	 */
313 View Code Duplication
	static function recipe_notes_shortcode( $atts, $content = '' ) {
314
		$atts = shortcode_atts( array(
315
			'title' => '', // string.
316
		), $atts, 'recipe-notes' );
317
318
		$html = '';
319
320
		// Print a title if one exists.
321
		if ( '' !== $atts['title'] ) {
322
			$html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
323
		}
324
325
		$html .= '<div class="jetpack-recipe-notes">';
326
327
		// Format content using list functionality, if desired.
328
		$html .= self::output_list_content( $content, 'notes' );
329
330
		$html .= '</div>';
331
332
		// Sanitize html.
333
		$html = wp_kses_post( $html );
334
335
		// Return the HTML block.
336
		return $html;
337
	}
338
339
	/**
340
	 * Our [recipe-ingredients] shortcode.
341
	 * Outputs notes, styled in a div.
342
	 *
343
	 * @param array  $atts    Array of shortcode attributes.
344
	 * @param string $content Post content.
345
	 *
346
	 * @return string HTML for recipe ingredients shortcode.
347
	 */
348 View Code Duplication
	static function recipe_ingredients_shortcode( $atts, $content = '' ) {
349
		$atts = shortcode_atts( array(
350
			'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
351
		), $atts, 'recipe-ingredients' );
352
353
		$html = '<div class="jetpack-recipe-ingredients">';
354
355
		// Print a title unless the user has opted to exclude it.
356
		if ( 'false' !== $atts['title'] ) {
357
			$html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
358
		}
359
360
		// Format content using list functionality.
361
		$html .= self::output_list_content( $content, 'ingredients' );
362
363
		$html .= '</div>';
364
365
		// Sanitize html.
366
		$html = wp_kses_post( $html );
367
368
		// Return the HTML block.
369
		return $html;
370
	}
371
372
	/**
373
	 * Reusable function to check for shortened formatting.
374
	 * Basically, users can create lists with the following shorthand:
375
	 * - item one
376
	 * - item two
377
	 * - item three
378
	 * And we'll magically convert it to a list. This has the added benefit
379
	 * of including itemprops for the recipe schema.
380
	 *
381
	 * @param string $content HTML content.
382
	 * @param string $type    Type of list.
383
	 *
384
	 * @return string content formatted as a list item
385
	 */
386
	static function output_list_content( $content, $type ) {
387
		$html = '';
388
389
		switch ( $type ) {
390
			case 'directions' :
391
				$list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
392
				$itemprop              = ' itemprop="recipeInstructions"';
393
				$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...
394
				break;
395
			case 'ingredients' :
396
				$list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
397
				$itemprop              = '';
398
				$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...
399
				break;
400
			default:
401
				$list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
402
				$itemprop              = '';
403
				$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...
404
		}
405
406
		// Check to see if the user is trying to use shortened formatting.
407
		if (
408
			strpos( $content, '&#8211;' ) !== false ||
409
			strpos( $content, '&#8212;' ) !== false ||
410
			strpos( $content, '-' )       !== false ||
411
			strpos( $content, '*' )       !== false ||
412
			strpos( $content, '#' )       !== false ||
413
			strpos( $content, '–' )       !== false || // ndash.
414
			strpos( $content, '—' )       !== false || // mdash.
415
			preg_match( '/\d+\.\s/', $content )
416
		) {
417
			// Remove breaks and extra whitespace.
418
			$content = str_replace( "<br />\n", "\n", $content );
419
			$content = trim( $content );
420
421
			$ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
422
			$ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
423
424
			preg_match_all( $ul_pattern, $content, $ul_matches );
425
			preg_match_all( $ol_pattern, $content, $ol_matches );
426
427
			if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
428
429
				if ( 0 !== count( $ol_matches[0] ) ) {
430
					$listtype = 'ol';
431
					$list_item_pattern = $ol_pattern;
432
				} else {
433
					$listtype = 'ul';
434
					$list_item_pattern = $ul_pattern;
435
				}
436
				$html .= '<' . $listtype . $itemprop . '>';
437
				$html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
438
				$html .= '</' . $listtype . '>';
439
440
				// Strip out any empty <p> tags and stray </p> tags, because those are just silly.
441
				$empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
442
				$html = preg_replace( $empty_p_pattern, '', $html );
443
			} else {
444
				$html .= do_shortcode( $content );
445
			}
446
		} else {
447
			$html .= do_shortcode( $content );
448
		}
449
450
		// Return our formatted content.
451
		return $html;
452
	}
453
454
	/**
455
	 * Our [recipe-directions] shortcode.
456
	 * Outputs directions, styled in a div.
457
	 *
458
	 * @param array  $atts    Array of shortcode attributes.
459
	 * @param string $content Post content.
460
	 *
461
	 * @return string HTML for recipe directions shortcode.
462
	 */
463 View Code Duplication
	static function recipe_directions_shortcode( $atts, $content = '' ) {
464
		$atts = shortcode_atts( array(
465
				'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
466
		), $atts, 'recipe-directions' );
467
468
		$html = '<div class="jetpack-recipe-directions">';
469
470
		// Print a title unless the user has specified to exclude it.
471
		if ( 'false' !== $atts['title'] ) {
472
			$html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
473
		}
474
475
		// Format content using list functionality.
476
		$html .= self::output_list_content( $content, 'directions' );
477
478
		$html .= '</div>';
479
480
		// Sanitize html.
481
		$html = wp_kses_post( $html );
482
483
		// Return the HTML block.
484
		return $html;
485
	}
486
487
	/**
488
	 * Use $themecolors array to style the Recipes shortcode
489
	 *
490
	 * @print style block
491
	 * @return string $style
492
	 */
493
	function themecolor_styles() {
494
		global $themecolors;
495
		$style = '';
496
497
		if ( isset( $themecolors ) ) {
498
			$style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
499
			$style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
500
		}
501
502
		return $style;
503
	}
504
505
}
506
507
new Jetpack_Recipes();
508