Files
wordpress-preseed/wp-content/plugins/jetpack/modules/shortcodes/recipe.php
2019-10-24 00:12:05 +02:00

538 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
use Automattic\Jetpack\Assets;
/**
* Embed recipe 'cards' in post, with basic styling and print functionality
*
* To Do
* - defaults settings
* - basic styles/themecolor styles
* - validation/sanitization
* - print styles
*
* @package Jetpack
*/
/**
* Register and display Recipes in posts.
*/
class Jetpack_Recipes {
/**
* Have scripts and styles been enqueued already.
*
* @var bool
*/
private $scripts_and_style_included = false;
/**
* Constructor
*/
public function __construct() {
add_action( 'init', array( $this, 'action_init' ) );
add_filter( 'wp_kses_allowed_html', array( $this, 'add_recipes_kses_rules' ), 10, 2 );
}
/**
* Add Schema-specific attributes to our allowed tags in wp_kses,
* so we can have better Schema.org compliance.
*
* @param array $allowedtags Array of allowed HTML tags in recipes.
* @param array $context Context to judge allowed tags by.
*/
public function add_recipes_kses_rules( $allowedtags, $context ) {
if ( in_array( $context, array( '', 'post', 'data' ) ) ) : // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
// Create an array of all the tags we'd like to add the itemprop attribute to.
$tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time' );
foreach ( $tags as $tag ) {
$allowedtags = $this->add_kses_rule(
$allowedtags,
$tag,
array(
'class' => array(),
'itemprop' => array(),
'datetime' => array(),
)
);
}
// Allow itemscope and itemtype for divs.
$allowedtags = $this->add_kses_rule(
$allowedtags,
'div',
array(
'class' => array(),
'itemscope' => array(),
'itemtype' => array(),
)
);
endif;
return $allowedtags;
}
/**
* Function to add a new property rule to our kses array.
* Used by add_recipe_kses_rules() above.
*
* @param array $all_tags Array of allowed HTML tags in recipes.
* @param string $tag New HTML tag to add to the array of allowed HTML.
* @param array $rules Array of allowed attributes for that HTML tag.
*/
private function add_kses_rule( $all_tags, $tag, $rules ) {
// If the tag doesn't already exist, add it.
if ( ! isset( $all_tags[ $tag ] ) ) {
$all_tags[ $tag ] = array();
}
// Merge the new tags with existing tags.
$all_tags[ $tag ] = array_merge( $all_tags[ $tag ], $rules );
return $all_tags;
}
/**
* Register our shortcode and enqueue necessary files.
*/
public function action_init() {
// Enqueue styles if [recipe] exists.
add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
// Render [recipe], along with other shortcodes that can be nested within.
add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
}
/**
* Enqueue scripts and styles
*/
public function add_scripts() {
if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
return;
}
foreach ( $GLOBALS['posts'] as $p ) {
if ( has_shortcode( $p->post_content, 'recipe' ) ) {
$this->scripts_and_style_included = true;
break;
}
}
if ( ! $this->scripts_and_style_included ) {
return;
}
wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
// add $themecolors-defined styles.
wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
wp_enqueue_script(
'jetpack-recipes-printthis',
Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
array( 'jquery' ),
'20170202',
false
);
wp_enqueue_script(
'jetpack-recipes-js',
Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
array( 'jquery', 'jetpack-recipes-printthis' ),
'20131230',
false
);
$title_var = wp_title( '|', false, 'right' );
$rtl = is_rtl() ? '-rtl' : '';
$print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
wp_localize_script(
'jetpack-recipes-js',
'jetpack_recipes_vars',
array(
'pageTitle' => $title_var,
'loadCSS' => $print_css_var,
)
);
}
/**
* Our [recipe] shortcode.
* Prints recipe data styled to look good on *any* theme.
*
* @param array $atts Array of shortcode attributes.
* @param string $content Post content.
*
* @return string HTML for recipe shortcode.
*/
public static function recipe_shortcode( $atts, $content = '' ) {
$atts = shortcode_atts(
array(
'title' => '', // string.
'servings' => '', // intval.
'time' => '', // string.
'difficulty' => '', // string.
'print' => '', // string.
'source' => '', // string.
'sourceurl' => '', // string.
'image' => '', // string.
'description' => '', // string.
),
$atts,
'recipe'
);
return self::recipe_shortcode_html( $atts, $content );
}
/**
* The recipe output
*
* @param array $atts Array of shortcode attributes.
* @param string $content Post content.
*
* @return string HTML output
*/
private static function recipe_shortcode_html( $atts, $content = '' ) {
$html = '<div class="hrecipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
// Print the recipe title if exists.
if ( '' !== $atts['title'] ) {
$html .= '<h3 class="jetpack-recipe-title" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
}
// Print the recipe meta if exists.
if (
'' !== $atts['servings']
|| '' !== $atts['time']
|| '' !== $atts['difficulty']
|| '' !== $atts['print']
) {
$html .= '<ul class="jetpack-recipe-meta">';
if ( '' !== $atts['servings'] ) {
$html .= sprintf(
'<li class="jetpack-recipe-servings" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
esc_html_x( 'Servings', 'recipe', 'jetpack' ),
esc_html( $atts['servings'] )
);
}
if ( '' !== $atts['time'] ) {
// Get a time that's supported by Schema.org.
$duration = WPCOM_JSON_API_Date::format_duration( $atts['time'] );
// If no duration can be calculated, let's output what the user provided.
if ( empty( $duration ) ) {
$duration = $atts['time'];
}
$html .= sprintf(
'<li class="jetpack-recipe-time">
<time itemprop="totalTime" datetime="%3$s"><strong>%1$s: </strong>%2$s</time>
</li>',
esc_html_x( 'Time', 'recipe', 'jetpack' ),
esc_html( $atts['time'] ),
esc_attr( $duration )
);
}
if ( '' !== $atts['difficulty'] ) {
$html .= sprintf(
'<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
esc_html( $atts['difficulty'] )
);
}
if ( '' !== $atts['source'] ) {
$html .= sprintf(
'<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
esc_html_x( 'Source', 'recipe', 'jetpack' )
);
if ( '' !== $atts['sourceurl'] ) :
// Show the link if we have one.
$html .= sprintf(
'<a href="%2$s">%1$s</a>',
esc_html( $atts['source'] ),
esc_url( $atts['sourceurl'] )
);
else :
// Skip the link.
$html .= sprintf(
'%1$s',
esc_html( $atts['source'] )
);
endif;
$html .= '</li>';
}
if ( 'false' !== $atts['print'] ) {
$html .= sprintf(
'<li class="jetpack-recipe-print"><a href="#">%1$s</a></li>',
esc_html_x( 'Print', 'recipe', 'jetpack' )
);
}
$html .= '</ul>';
}
// Output the image, if we have one.
if ( '' !== $atts['image'] ) {
$html .= sprintf(
'<img class="jetpack-recipe-image" itemprop="image" src="%1$s" />',
esc_url( $atts['image'] )
);
}
// Output the description, if we have one.
if ( '' !== $atts['description'] ) {
$html .= sprintf(
'<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
esc_html( $atts['description'] )
);
}
// Print content between codes.
$html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
// Close it up.
$html .= '</div>';
// If there is a recipe within a recipe, remove the shortcode.
if ( has_shortcode( $html, 'recipe' ) ) {
remove_shortcode( 'recipe' );
}
// Sanitize html.
$html = wp_kses_post( $html );
// Return the HTML block.
return $html;
}
/**
* Our [recipe-notes] shortcode.
* Outputs ingredients, styled in a div.
*
* @param array $atts Array of shortcode attributes.
* @param string $content Post content.
*
* @return string HTML for recipe notes shortcode.
*/
public static function recipe_notes_shortcode( $atts, $content = '' ) {
$atts = shortcode_atts(
array(
'title' => '', // string.
),
$atts,
'recipe-notes'
);
$html = '';
// Print a title if one exists.
if ( '' !== $atts['title'] ) {
$html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
}
$html .= '<div class="jetpack-recipe-notes">';
// Format content using list functionality, if desired.
$html .= self::output_list_content( $content, 'notes' );
$html .= '</div>';
// Sanitize html.
$html = wp_kses_post( $html );
// Return the HTML block.
return $html;
}
/**
* Our [recipe-ingredients] shortcode.
* Outputs notes, styled in a div.
*
* @param array $atts Array of shortcode attributes.
* @param string $content Post content.
*
* @return string HTML for recipe ingredients shortcode.
*/
public static function recipe_ingredients_shortcode( $atts, $content = '' ) {
$atts = shortcode_atts(
array(
'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
),
$atts,
'recipe-ingredients'
);
$html = '<div class="jetpack-recipe-ingredients">';
// Print a title unless the user has opted to exclude it.
if ( 'false' !== $atts['title'] ) {
$html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
}
// Format content using list functionality.
$html .= self::output_list_content( $content, 'ingredients' );
$html .= '</div>';
// Sanitize html.
$html = wp_kses_post( $html );
// Return the HTML block.
return $html;
}
/**
* Reusable function to check for shortened formatting.
* Basically, users can create lists with the following shorthand:
* - item one
* - item two
* - item three
* And we'll magically convert it to a list. This has the added benefit
* of including itemprops for the recipe schema.
*
* @param string $content HTML content.
* @param string $type Type of list.
*
* @return string content formatted as a list item
*/
private static function output_list_content( $content, $type ) {
$html = '';
switch ( $type ) {
case 'directions':
$list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
$itemprop = ' itemprop="recipeInstructions"';
$listtype = 'ol';
break;
case 'ingredients':
$list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
$itemprop = '';
$listtype = 'ul';
break;
default:
$list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
$itemprop = '';
$listtype = 'ul';
}
// Check to see if the user is trying to use shortened formatting.
if (
strpos( $content, '&#8211;' ) !== false ||
strpos( $content, '&#8212;' ) !== false ||
strpos( $content, '-' ) !== false ||
strpos( $content, '*' ) !== false ||
strpos( $content, '#' ) !== false ||
strpos( $content, '' ) !== false || // ndash.
strpos( $content, '—' ) !== false || // mdash.
preg_match( '/\d+\.\s/', $content )
) {
// Remove breaks and extra whitespace.
$content = str_replace( "<br />\n", "\n", $content );
$content = trim( $content );
$ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
$ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
preg_match_all( $ul_pattern, $content, $ul_matches );
preg_match_all( $ol_pattern, $content, $ol_matches );
if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
if ( 0 !== count( $ol_matches[0] ) ) {
$listtype = 'ol';
$list_item_pattern = $ol_pattern;
} else {
$listtype = 'ul';
$list_item_pattern = $ul_pattern;
}
$html .= '<' . $listtype . $itemprop . '>';
$html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
$html .= '</' . $listtype . '>';
// Strip out any empty <p> tags and stray </p> tags, because those are just silly.
$empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
$html = preg_replace( $empty_p_pattern, '', $html );
} else {
$html .= do_shortcode( $content );
}
} else {
$html .= do_shortcode( $content );
}
// Return our formatted content.
return $html;
}
/**
* Our [recipe-directions] shortcode.
* Outputs directions, styled in a div.
*
* @param array $atts Array of shortcode attributes.
* @param string $content Post content.
*
* @return string HTML for recipe directions shortcode.
*/
public static function recipe_directions_shortcode( $atts, $content = '' ) {
$atts = shortcode_atts(
array(
'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
),
$atts,
'recipe-directions'
);
$html = '<div class="jetpack-recipe-directions">';
// Print a title unless the user has specified to exclude it.
if ( 'false' !== $atts['title'] ) {
$html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
}
// Format content using list functionality.
$html .= self::output_list_content( $content, 'directions' );
$html .= '</div>';
// Sanitize html.
$html = wp_kses_post( $html );
// Return the HTML block.
return $html;
}
/**
* Use $themecolors array to style the Recipes shortcode
*
* @print style block
* @return string $style
*/
public function themecolor_styles() {
global $themecolors;
$style = '';
if ( isset( $themecolors ) ) {
$style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
$style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
}
return $style;
}
}
new Jetpack_Recipes();