/** * @param $content */ public function __construct($content) { $dom = new EasyRecipePlusDOMDocument("", false); $dom->registerNodeClass('DOMDocument', 'EasyRecipePlusDOMDocument'); $dom->registerNodeClass('DOMElement', 'EasyRecipePlusMicrodataDOMElement'); $dom->preserveWhiteSpace = false; @$dom->loadHTML($content); $this->dom = $dom; }
/** * If there's an EasyRecipe in the content, load the HTML and pre-process, else just return * * @param $content * @param bool $load */ public function __construct($content, $load = true) { /** * If there's no EasyRecipe, just return */ if (!@preg_match(self::regexEasyRecipe, $content)) { return; } /** * Load the html - make sure we could parse it */ parent::__construct($content, $load); if (!$this->isValid()) { return; } /** * Find the easyrecipe(s) */ $this->easyrecipes = $this->getElementsByClassName('easyrecipe'); /** * Sanity check - make sure we could actually find at least one */ if (count($this->easyrecipes) == 0) { // echo "<!-- ER COUNT = 0 -->\n"; return; } /** * This is a valid easyrecipe post * Find a version number - the version will be the same for every recipe in a multi recipe post so just get the first */ $this->isEasyRecipe = true; /* @var $node DOMElement */ $node = $this->getElementByClassName("endeasyrecipe", "div", $this->easyrecipes[0], false); $this->recipeVersion = $node->nodeValue; /* * See if this post has already been formatted. * Wordpress replaces the parent post_content with the autosave post content (as already formatted by us) on a preview. * so we need to know if this post has already been formatted. This is a pretty icky way of doing it since it relies * on the style template having a specific title attribute on the endeasyrecipe div - need to make this more robust */ $this->isFormatted = $node !== null && $node->hasAttribute('title'); }
/** * Process any EasyRecipes in all the posts on the page * We need to do this here rather than in the_content hook because by then it's too late to queue up the scripts/styles we'll need * * @param $posts * * @return array */ function thePosts($posts) { /* @var $wp_rewrite WP_Rewrite */ global $wp_rewrite; /** @global $wpdb wpdb */ global $wpdb; /** * We don't want to process anything if it's a missing URL */ if (is_404()) { return $posts; } global $shortcode_tags; $guestpost = null; $newPosts = array(); /** * Process each post and replace placeholders with relevant data */ foreach ($posts as $post) { /** * Have we already processed this post? */ if (isset($this->easyrecipes[$post->ID])) { $post->post_content = $this->postContent[$post->ID]; $newPosts[] = $post; continue; } /** * We may have to change the rating method (e.g. for Ziplist recipes) so make a local copy */ $this->ratingMethod = $this->settings->ratings; /** * Do we want to take over processing of other recipe plugins posts? * If this is a post from another plugin and we are displaying it, make the rating method "Self Rated" unless ratings are disabled */ if (!is_admin()) { $this->converted = false; if ($this->settings->displayZiplist) { $post->post_content = preg_replace_callback('/\\[amd-(zlrecipe)-recipe:(\\d+)\\]/', array($this, 'doConvert'), $post->post_content); if ($this->converted && $this->settings->ratings != 'Disabled') { $this->ratingMethod = 'SelfRated'; } } if ($this->settings->displayRecipeCard) { $post->post_content = preg_replace_callback('/\\[(yumprint)-recipe id=[\'"]"(\\d+)[\'"]\\]/', array($this, 'doConvert'), $post->post_content); if ($this->converted && $this->settings->ratings != 'Disabled') { $this->ratingMethod = 'SelfRated'; } } if ($this->settings->displayGMC) { $post->post_content = preg_replace_callback('/\\[(gmc_recipe) (\\d+)\\]/', array($this, 'doConvert'), $post->post_content); if ($this->converted && $this->settings->ratings != 'Disabled') { $this->ratingMethod = 'SelfRated'; } } if ($this->settings->displayUltimateRecipe) { if ($post->post_type == 'recipe') { $post->post_content = "[ultimate-recipe id='{$post->ID}']"; $post->post_type = 'post'; } $post->post_content = preg_replace_callback('/\\[(ultimate-recipe) id=["\'](\\d+|random)["\']\\]/i', array($this, 'doConvert'), $post->post_content); if ($this->converted && $this->settings->ratings != 'Disabled') { $this->ratingMethod = 'SelfRated'; } } if ($this->settings->displayRecipage) { /** * Do a quick check before we go to the expense of instantiating a DOMDocument */ if (strpos($post->post_content, 'hrecipe f') !== false) { $document = new EasyRecipePlusDOMDocument($post->post_content); if ($document->isValid()) { /** @var DOMElement $hrecipe */ $hrecipe = $document->getElementByClassName('hrecipe'); if ($hrecipe) { $matches = array(); $matches[1] = 'recipage'; $matches[2] = $post->ID; $convertedRecipe = $this->doConvert($matches); /** @var DOMDocumentFragment $fragment */ $fragment = $document->createDocumentFragment(); $fragment->appendXML($convertedRecipe); $hrecipe->parentNode->replaceChild($fragment, $hrecipe); $content = $document->saveHTML(); $post->post_content = preg_replace('%^.*</head><body>(.*)</body></html>\\s*$%sm', '$1', $content); } } } } } /** * Handle the guest post shortcodes here because WP doesn't process them until much later * We may need to redirect so we need to do it before anything else has a chance to do output * We may also need to process a recipe */ if ($post->ID == $this->settings->gpDetailsPage || $post->ID == $this->settings->gpEntryPage || $post->ID == $this->settings->gpThanksPage) { if (empty($guestPost)) { $guestPost = new EasyRecipePlusGuestPost($this); } $gpResult = $guestPost->process($post); /** * If $guestPost->process() returns something, we processed the GP entry page * In this case, all we need to do is save it in $newPosts and continue * Otherwise, continue with processing since there may conceivably be an EasyRecipe on the post/page * * TODO - do we really need to do this? Won't it get picked up just below anyway? */ if ($gpResult) { $newPosts[] = $gpResult; continue; } } $postDOM = new EasyRecipePlusDocument($post->post_content); if (!$postDOM->isEasyRecipe) { if (strncasecmp($post->post_content, '[easyrecipe_page]', 17) === 0) { $this->easyrecipes[$post->ID] = true; $post->post_content = str_replace('[easyrecipe_page]', '', $post->post_content); } $newPosts[] = $post; continue; } $postDOM->setSettings($this->settings); /** * Mark this post as an easyrecipe so that the comment and rating processing know */ $this->easyrecipes[$post->ID] = true; /** * Make sure we haven't already formatted this post. This can happen in preview mode where WP replaces the post_content * of the parent with the autosave content which we've already processed. * If this is the case, save the formatted code and mark this post as having been processed * TODO - are there implications for the object cache for themes that re-read posts? */ if ($postDOM->isFormatted) { $this->postContent[$post->ID] = $post->post_content; $newPosts[] = $post; continue; } /** * Fix possibly broken times in older posts * Fix the Cholesterol typo oops in early versions */ if ($postDOM->recipeVersion < '3') { $postDOM->fixTimes("preptime"); $postDOM->fixTimes("cooktime"); $postDOM->fixTimes("duration"); $postDOM->setParentValueByClassName("cholestrol", $this->settings->lblCholesterol, "Cholestrol"); } $data = new stdClass(); /** * Get the ratings from the comment meta table if we use the EasyRecipe comment method * Other rating methods are handled in EasyRecipePlusDocument->applyStyle() * hasRatings is left unset for Self Rating */ if ($this->ratingMethod == 'EasyRecipe') { $q = "SELECT COUNT(*) AS count, SUM(meta_value) AS sum FROM {$wpdb->comments} JOIN {$wpdb->commentmeta} ON {$wpdb->commentmeta}.comment_id = {$wpdb->comments}.comment_ID "; $q .= "WHERE comment_approved = 1 AND meta_key = 'ERRating' AND comment_post_ID = {$post->ID} AND meta_value > 0"; $ratings = $wpdb->get_row($q); if ((int) $ratings->count > 0) { $data->ratingCount = $ratings->count; $data->ratingValue = number_format($ratings->sum / $ratings->count, 1); $data->ratingPC = $data->ratingValue * 100 / 5; $data->hasRating = true; } else { $data->hasRating = false; } } else { if ($this->ratingMethod == 'Disabled') { $data->hasRating = false; } } switch ($this->settings->saveButton) { case 'Ziplist': $data->saveButtonJS = self::ZIPLISTJS; $data->saveButton = sprintf(self::ZIPLISTBUTTON, $this->settings->ziplistPartnerKey, urlencode(get_permalink($post->ID)), $this->settings->lblSave); $data->hasSave = true; break; case 'BigOven': $data->saveButtonJS = ''; $data->saveButton = sprintf(self::BIGOVENBUTTON, self::$EasyRecipePlusUrl); $data->hasSave = true; break; } $this->settings->getLabels($data); $data->hasLinkback = $this->settings->allowLink; $data->displayPrint = $this->settings->displayPrint; $data->style = $this->styleName; $data->title = $post->post_title; $data->blogname = get_option("blogname"); // TODO - do all this stuff at initialise time? $data->siteURL = $this->homeURL; /** * If the site isn't using permalinks then just pass the print stuff as a qurerystring param */ if ($wp_rewrite->using_permalinks()) { $data->sitePrintURL = $data->siteURL; } else { $data->sitePrintURL = $data->siteURL . "?"; } $data->postID = $post->ID; $data->recipeurl = get_permalink($post->ID); $data->convertFractions = $this->settings->convertFractions; if ($this->styleName[0] == '_') { $styleName = substr($this->styleName, 1); $templateFile = $this->settings->customTemplates . "/styles/{$styleName}/style.html"; } else { $templateFile = self::$EasyRecipePlusDir . "/styles/{$this->styleName}/style.html"; } $template = new EasyRecipePlusTemplate($templateFile); $data->isLoggedIn = is_user_logged_in(); /** * Apply styles to the recipe data and return the content with recipes replace by a shortcode and also each recipe's HTML * Also keep a copy so we don't have to reformat in the case where the theme asks for the same post again * * This didn't work! Some themes don't call the_content() (esp for excerpts) so we can't rely on hooking into that to supply the formatted html * We need to do it right here - it seems that the_posts is the only reliable place to replace the base recipe HTML with the formatted recipe HTML */ /** * Replace the original content with the one that has the easyrecipe(s) nicely formatted and marked up * Also keep a copy so we don't have to reformat in the case where the theme asks for the same post again */ $this->postContent[$post->ID] = $post->post_content = $postDOM->applyStyle($template, $data); /** * If we haven't already done so, hook into the_content filter to stop wpauto() messing with recipe HTML */ if (empty($shortcode_tags['easyrecipe'])) { add_filter('the_content', array($this, 'theContent'), 0); add_shortcode('easyrecipe', array($this, 'replaceRecipeShortcode')); } /** * Some themes do a get_post() again instead of using the posts as modified by plugins * So make sure our modified post is in cache so the get_post() picks up the modified version not the original * Need to do both add and replace since add doesn't replace and replace doesn't add and we can't be sure if the cache key exists at this point */ wp_cache_add($post->ID, $post, 'posts'); wp_cache_replace($post->ID, $post, 'posts'); $newPosts[] = $post; } return $newPosts; }
/** * Process the template text * * @param null $data * @param int $options * @return mixed|string */ function replace($data = null, $options = 0) { /** * If we have replacements, pre-process the template and remove conditionally INCLUDEd stuff */ if ($data === null) { $data = new stdClass(); } $currentPosition = 0; $this->opText = ''; $inText = $this->inText; $firstType = null; /** * We return from within this loop when we have nothing left to process */ while (true) { /** * Look for stuff to replace and find the first of them */ $firstPosition = strlen($inText); $varPosition = strpos($inText, $this->delimiter, $currentPosition); if ($varPosition !== false) { $firstPosition = $varPosition; $firstType = self::VARIABLEREPLACE; } $repeatPosition = strpos($inText, '<!-- START REPEAT ', $currentPosition); if ($repeatPosition !== false && $repeatPosition < $firstPosition) { $firstPosition = $repeatPosition; $firstType = self::REPEATREPLACE; } $includeifPosition = strpos($inText, '<!-- START INCLUDEIF ', $currentPosition); if ($includeifPosition !== false && $includeifPosition < $firstPosition) { $firstPosition = $includeifPosition; $firstType = self::INCLUDEIF; } $startStripPosition = strpos($inText, '<!-- START STRIP WHITESPACE ', $currentPosition); if ($startStripPosition !== false && $startStripPosition < $firstPosition) { $firstPosition = $startStripPosition; $firstType = self::STARTSTRIP; } $endStripPosition = strpos($inText, '<!-- END STRIP WHITESPACE ', $currentPosition); if ($endStripPosition !== false && $endStripPosition < $firstPosition) { $firstPosition = $endStripPosition; $firstType = self::ENDSTRIP; } /** * If there's nothing to do, just return what we've got */ if ($firstPosition == strlen($inText)) { /** * Copy any remaining input over to the output */ $this->opText .= substr($inText, $currentPosition); /* * If there's any block to whitespace strip, do it * Tidy up the start/stop positions first to make it easy to process * This allows overlapping and unterminated stip blocks */ if (count($this->stripWhitespace) > 0) { $stripBlocks = array(); $startPosition = -1; foreach ($this->stripWhitespace as $position) { /** * End strip? */ if ($position < 0) { /** * If there's no previous unterminated START STRIP, ignore this END */ if ($startPosition == -1) { continue; } $stripBlocks[] = array($startPosition, abs($position)); $startPosition = -1; } else { /** * If there's a previous unterminated START STRIP, ignore this one */ if ($startPosition != -1) { continue; } $startPosition = $position; } } /** * Allow for a missing END STRIP */ if ($startPosition != -1) { $stripBlocks[] = array($startPosition, strlen($this->opText)); } /** * Actually strip out whitespace in the strip blocks */ $currentPosition = 0; $strippedText = ''; foreach ($stripBlocks as $stripBlock) { $strippedText .= substr($this->opText, $currentPosition, $stripBlock[0] - $currentPosition); $text = substr($this->opText, $stripBlock[0], $stripBlock[1] - $stripBlock[0]); $text = preg_replace('/>\\s+</', '><', $text); $strippedText .= trim($text); $currentPosition = $stripBlock[1]; } $strippedText .= substr($this->opText, $currentPosition, strlen($this->opText) - $currentPosition); $this->opText = $strippedText; } /** * If we aren't translating, then just (optionally) clean up whitespace and return */ if (!self::$translate || !class_exists('EasyRecipePlusDOMDocument')) { return $this->cleanWhitespace($this->opText, $options); } $doc = new EasyRecipePlusDOMDocument($this->opText, true); if (!$doc) { return $this->opText; } $xlates = $doc->getElementsByClassName('xlate'); if (count($xlates) == 0) { return $this->cleanWhitespace($this->opText, $options); } // FIXME - use gettext if no __ foreach ($xlates as $xlate) { $original = $doc->innerHTML($xlate); $translation = __($original, self::$textDomain); if ($translation != $original) { $xlate->nodeValue = $translation; } } $html = $doc->getHTML(true); return $this->cleanWhitespace($html, $options); } /** * Copy over everything up to the first thing we need to process */ $length = $firstPosition - $currentPosition; $this->opText .= substr($inText, $currentPosition, $length); $currentPosition = $firstPosition; /** * Get the thing to be replaced */ switch ($firstType) { /** * INCLUDEIF includes the code up to the matching END INCLUDEIF: * IF the condition variable exists and it's not false or null */ case self::INCLUDEIF: /** * Get the conditional. * Only check a smallish substring for efficiency * This limits include condition names to 20 characters */ $subString = substr($inText, $currentPosition, 60); if (preg_match('/<!-- START INCLUDEIF (!?)([_a-z][_0-9a-z]{0,31}) -->/i', $subString, $regs)) { $negate = $regs[1]; $trueFalse = $negate != '!'; $includeCondition = $regs[2]; } else { trigger_error("Malformed START INCLUDEIF at {$currentPosition} ({$subString})", E_USER_NOTICE); $this->opText .= "<"; $currentPosition++; continue; } $endInclude = "<!-- END INCLUDEIF {$negate}{$includeCondition} -->"; $endIncludeLength = strlen($endInclude); $endPosition = strpos($inText, $endInclude); if ($endPosition == false) { trigger_error(htmlspecialchars("'{$endInclude}'") . " not found", E_USER_NOTICE); $this->opText .= "<"; $currentPosition++; break; } /** * If the condition is met, just remove the INCLUDEIF comments * If the condition isn't met, remove everything up to the END INCLUDEIF * The condition must be present, and NOT false or NULL */ $condition = isset($data->{$includeCondition}) && $data->{$includeCondition} !== false && $data->{$includeCondition} !== null; if ($condition === $trueFalse) { $startInclude = "<!-- START INCLUDEIF {$negate}{$includeCondition} -->"; $startIncludeLength = strlen($startInclude); $inText = substr($inText, 0, $currentPosition) . substr($inText, $currentPosition + $startIncludeLength, $endPosition - $currentPosition - $startIncludeLength) . substr($inText, $endPosition + $endIncludeLength); } else { $inText = substr($inText, 0, $currentPosition) . substr($inText, $endPosition + $endIncludeLength); } break; /** * Remove whitespace between tags. * Useful to remove unwanted significant HTML whitespace that may have been introduced by auto formatting the source template */ /** * Remove whitespace between tags. * Useful to remove unwanted significant HTML whitespace that may have been introduced by auto formatting the source template */ case self::STARTSTRIP: $currentPosition += 31; $this->stripWhitespace[] = strlen($this->opText); break; /** * Save the output position at which to stop stripping. -ve indicates that it's an end position */ /** * Save the output position at which to stop stripping. -ve indicates that it's an end position */ case self::ENDSTRIP: $currentPosition += 29; $this->stripWhitespace[] = -strlen($this->opText); break; /** * A variable is a valid PHP variable name (limited to 20 chars) between delimiters * If we don't find a valid name, copy over the delimiter and continue * FIXME - fall back to caller's vars if it doesn't exist */ /** * A variable is a valid PHP variable name (limited to 20 chars) between delimiters * If we don't find a valid name, copy over the delimiter and continue * FIXME - fall back to caller's vars if it doesn't exist */ case self::VARIABLEREPLACE: $s = substr($inText, $currentPosition, 34); if (!preg_match("/^{$this->delimiter}([_a-z][_0-9a-z]{0,31}){$this->delimiter}/si", $s, $regs)) { $this->opText .= $this->delimiter; $currentPosition++; continue; } /** * If we don't have a match for the variable, just assume it's not what we wanted to do * so put the string we matched back into the output and continue from the trailing delimiter */ $varName = $regs[1]; if (!isset($data->{$varName})) { $this->opText .= $this->delimiter . $varName; $currentPosition += strlen($varName) + 1; continue; } /** * Got a match - replace the <delimiter>...<delimiter> with the vars stuff * We *could* pass this on for recursive processing, but it's not something we would normally want to do * Maybe have a special naming convention for vars we want to do recursively? */ $this->opText .= $data->{$varName}; $currentPosition += strlen($varName) + 2; break; /** * We've seen a start repeat. * Find the name of the repeat (limited to 20 chars) * If we can't find the name, assume it's not what we want and continue * * Look for a valid START REPEAT in the next 45 characters */ /** * We've seen a start repeat. * Find the name of the repeat (limited to 20 chars) * If we can't find the name, assume it's not what we want and continue * * Look for a valid START REPEAT in the next 45 characters */ case self::REPEATREPLACE: $s = substr($inText, $currentPosition, 45); if (!preg_match('/<!-- START REPEAT ([_a-zA-Z][_0-9a-zA-Z]{0,19}) -->/m', $s, $regs)) { $this->opText .= '<'; $currentPosition++; continue; } $rptName = $regs[1]; /** * Make sure we have a matching key and it's an array */ if (!isset($data->{$rptName}) || !is_array($data->{$rptName})) { $this->opText .= '<'; $currentPosition++; continue; } /** * Now try to find the end of this repeat */ $currentPosition += strlen($rptName) + 22; $rptEnd = strpos($inText, "<!-- END REPEAT {$rptName} -->", $currentPosition); if ($rptEnd === false) { $this->opText .= '<!-- START REPEAT $rptName -->'; trigger_error("END REPEAT not found for {$rptName}", E_USER_NOTICE); continue; } /** * Do the repeat processing. * For each item in the repeated array, process as a new template */ $rptLength = $rptEnd - $currentPosition; $rptString = substr($inText, $currentPosition, $rptLength); $rptVars = $data->{$rptName}; for ($i = 0; $i < count($rptVars); $i++) { $saveTranslate = self::$translate; self::$translate = false; $rpt = new EasyRecipePlusTemplate($rptString, self::TEXT, $this->delimiter); $this->opText .= $rpt->replace($rptVars[$i], $options); self::$translate = $saveTranslate; } /** * Step over the end repeat */ $currentPosition += strlen($rptName) + $rptLength + 20; break; } } return ''; }
private function doConvert($postID, $postType) { /** @global $wpdb wpdb */ global $wpdb; $result = new stdClass(); switch ($postType) { case 'ultimate-recipe': $result->recipe = new stdClass(); if ($postID == 'random') { $posts = get_posts(array('post_type' => 'recipe', 'nopaging' => true)); $post = $posts[array_rand($posts)]; } else { $post = get_post($postID); } $recipe = get_post_custom($post->ID); $user = get_userdata($post->post_author); $image = wp_get_attachment_image_src(get_post_thumbnail_id($post->ID), 'full'); $cuisine = wp_get_object_terms($post->ID, 'cuisine'); if ($cuisine instanceof WP_Error) { register_taxonomy('cuisine', 'recipe'); $cuisine = wp_get_object_terms($post->ID, 'cuisine'); } $course = wp_get_object_terms($post->ID, 'course'); if ($course instanceof WP_Error) { register_taxonomy('course', 'recipe'); $course = wp_get_object_terms($post->ID, 'course'); } /** * Very dodgy way of processing times - not sure what else we can do! * Times other than "x minutes" are going to be unparsable reliably * Will have to adjust this based on real user values as we come across them * * Try for xlated "minutes" first * If that doesn't work, at least try some likely English duration specifiers */ $xMinutes = __('minutes', 'wp-ultimate-recipe'); $timeText = !empty($recipe['recipe_prep_time_text']) ? $recipe['recipe_prep_time_text'][0] : ''; if ($timeText == $xMinutes || $timeText == 'minute') { $result->recipe->prep_time = 'PT' . $recipe['recipe_prep_time'][0] . 'M'; } elseif ($timeText == 'hours' || $timeText == 'hour') { $result->recipe->prep_time = 'PT' . $recipe['recipe_prep_time'][0] . 'H0M'; } else { $result->recipe->prep_time = ''; } $timeText = !empty($recipe['recipe_cook_time_text']) ? $recipe['recipe_cook_time_text'][0] : ''; if ($timeText == $xMinutes || $timeText == 'minute') { $result->recipe->cook_time = 'PT' . $recipe['recipe_cook_time'][0] . 'M'; } elseif ($timeText == 'hours' || $timeText == 'hour') { $result->recipe->cook_time = 'PT' . $recipe['recipe_cook_time'][0] . 'H0M'; } else { $result->recipe->cook_time = ''; } $result->recipe->recipe_image = !empty($image) ? $image[0] : ''; if (is_array($cuisine) && !empty($cuisine[0])) { $result->recipe->cuisine = htmlspecialchars($cuisine[0]->name); } if (is_array($course) && !empty($course[0])) { $result->recipe->mealType = htmlspecialchars($course[0]->name); } $result->recipe->recipe_title = !empty($recipe['recipe_title']) ? htmlspecialchars($recipe['recipe_title'][0]) : ''; /** @noinspection PhpUndefinedFieldInspection */ $result->recipe->author = htmlspecialchars($user->data->display_name); $result->recipe->summary = !empty($recipe['recipe_description']) ? htmlspecialchars($recipe['recipe_description'][0]) : ''; $notes = !empty($recipe['recipe_notes'][0]) ? preg_replace_callback('%<(strong|em)>(.*?)</\\1>%', array($this, 'notesConversion'), $recipe['recipe_notes'][0]) : ''; $result->recipe->notes = preg_replace('%<a ([^>]+)>(.*?)</a>%i', '[url $1]$2[/url]', $notes); $section = ''; $ingredients = array(); $urIngredients = @unserialize($recipe['recipe_ingredients'][0]); if (!$urIngredients) { $urIngredients = array(); } foreach ($urIngredients as $urIngredient) { if ($urIngredient['group'] != $section) { $section = $urIngredient['group']; $ingredients[] = '!' . htmlspecialchars($urIngredient['group']); } $ingredient = htmlspecialchars($urIngredient['amount']) . ' ' . htmlspecialchars($urIngredient['unit']) . ' ' . htmlspecialchars($urIngredient['ingredient']); if (!empty($urIngredient['notes'])) { $ingredient .= ' ' . htmlspecialchars($urIngredient['notes']); } $ingredients[] = $ingredient; } $result->ingredients = $ingredients; $section = ''; $instructions = array(); $urInstructions = @unserialize($recipe['recipe_instructions'][0]); if (!$urInstructions) { $urInstructions = array(); } foreach ($urInstructions as $urInstruction) { if ($urInstruction['group'] != $section) { $section = $urInstruction['group']; $instructions[] = '!' . htmlspecialchars($urInstruction['group']); } $instruction = htmlspecialchars($urInstruction['description']); if (!empty($urInstruction['image'])) { $instructionImage = wp_get_attachment_image_src($urInstruction['image'], 'large'); if (!empty($instructionImage)) { $instruction .= '[br][img src="' . $instructionImage[0] . '" width="' . $instructionImage[1] . '" height="' . $instructionImage[2] . '" /]'; } } $instructions[] = $instruction; } $result->recipe->instructions = implode("\n", $instructions); break; case 'recipage': /** @var WP_Post $post */ $post = $wpdb->get_row("SELECT * FROM " . $wpdb->posts . " WHERE ID=" . $postID); $content = $post->post_content; $document = new EasyRecipePlusDOMDocument($content); if (!$document->isValid()) { return null; } $hrecipe = $document->getElementByClassName('hrecipe'); if (!$hrecipe) { return null; } $result->recipe = new stdClass(); $result->recipe->total_time = ''; $result->recipe->serving_size = ''; $result->recipe->notes = ''; $result->recipe->calories = ''; $result->recipe->fat = ''; $result->recipe->rating = ''; /** @var DOMElement $element */ $element = $document->getElementByClassName('photo', 'img', $hrecipe); $result->recipe->recipe_image = $element != null ? $element->getAttribute('src') : ''; $element = $document->getElementByClassName('fn', '*', $hrecipe); $result->recipe->recipe_title = $element != null ? htmlspecialchars($element->textContent) : ''; $element = $document->getElementByClassName('author', '*', $hrecipe); $result->recipe->author = $element != null ? htmlspecialchars($element->textContent) : ''; $element = $document->getElementByClassName('summary', '*', $hrecipe); $result->recipe->summary = $element != null ? htmlspecialchars($element->textContent) : ''; $element = $document->getElementByClassName('yield', '*', $hrecipe); $result->recipe->yield = $element != null ? htmlspecialchars($element->textContent) : ''; $element = $document->getElementByClassName('preptime', '*', $hrecipe); $prepTime = $element != null ? $element->textContent : ''; $result->recipe->prep_time = $this->convertTimeString($prepTime); $element = $document->getElementByClassName('cooktime', '*', $hrecipe); $cookTime = $element != null ? $element->textContent : ''; $result->recipe->cook_time = $this->convertTimeString($cookTime); $result->ingredients = array(); $ingredients = $document->getElementsByClassName('ingredient', '*', $hrecipe); /** @var DOMElement $ingredient */ foreach ($ingredients as $ingredient) { $result->ingredients[] = trim($ingredient->textContent); } $instructions = array(); $elements = $document->getElementsByClassName('instruction', '*', $hrecipe); /** @var DOMElement $instruction */ foreach ($elements as $instruction) { $instructions[] = trim($instruction->textContent); } $result->recipe->instructions = implode("\n", $instructions); break; case 'recipeseo': $result->recipe = $wpdb->get_row("SELECT * FROM " . $wpdb->prefix . "amd_recipeseo_recipes WHERE recipe_id=" . $postID); $ingredients = $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "amd_recipeseo_ingredients WHERE recipe_id=" . $postID . " ORDER BY ingredient_id"); $result->ingredients = array(); foreach ($ingredients as $ingredient) { $result->ingredients[] = $ingredient->amount . " " . $ingredient->name; } break; case 'zlrecipe': $result->recipe = $wpdb->get_row("SELECT * FROM " . $wpdb->prefix . "amd_zlrecipe_recipes WHERE recipe_id=" . $postID); $result->recipe->nReviews = 1; $result->recipe->summary = $this->decodeMarkdown($result->recipe->summary); $result->recipe->notes = $this->decodeMarkdown($result->recipe->notes); /** * If only total time is specified, use it as the cook time * TODO - Do this for all plugins? */ if ($result->recipe->cook_time == '' && $result->recipe->prep_time == '') { $result->recipe->cook_time = $result->recipe->total_time; } $ingredients = explode("\n", str_replace("\r", "", $result->recipe->ingredients)); $result->ingredients = array(); foreach ($ingredients as $ingredient) { $ingredient = trim($ingredient); if ($ingredient != '') { if (preg_match('/^%([^\\s]+)/', $ingredient, $regs)) { if (($n = count($result->ingredients)) > 0 && $result->ingredients[$n - 1][0] != '!') { $result->ingredients[$n - 1] .= '[br][img src="' . $regs[1] . '"]'; continue; } else { $ingredient = '[img src="' . $regs[1] . '"]'; } } else { $ingredient = $this->decodeMarkdown($ingredient); } $result->ingredients[] = $ingredient; } } unset($result->recipe->ingredients); $instructions = explode("\n", str_replace("\r", "", $result->recipe->instructions)); $convertedInstructions = array(); foreach ($instructions as $instruction) { $instruction = trim($instruction); if ($instruction != '') { if (preg_match('/^%([^\\s]+)/', $instruction, $regs)) { if (($n = count($convertedInstructions)) > 0 && $convertedInstructions[$n - 1][0] != '!') { $convertedInstructions[$n - 1] .= '[br][img src="' . $regs[1] . '"]'; continue; } else { $instruction = '[img src="' . $regs[1] . '"]'; } } else { $instruction = $this->decodeMarkdown($instruction); } $convertedInstructions[] = $instruction; } } /** * For silly historical reasons, instructions are returned as a string */ $result->recipe->instructions = implode("\n", $convertedInstructions); break; case 'recipress': $meta = get_post_custom($postID); $result->recipe = new stdClass(); $result->ingredients = array(); $result->recipe->instructions = ''; /** @noinspection PhpUndefinedFunctionInspection */ $size = recipress_options('instruction_image_size'); $result->recipe->recipe_title = $meta['title'][0] ? $meta['title'][0] : ''; $photo = wp_get_attachment_image_src($meta['photo'][0], 'thumbnail', false); $result->recipe->recipe_image = $photo ? $photo[0] : ''; $result->recipe->summary = $meta['summary'][0] ? $meta['summary'][0] : ''; $terms = get_the_terms($postID, 'cuisine'); $result->recipe->cuisine = $terms[0]->name; $terms = get_the_terms($postID, 'course'); $result->recipe->mealType = $terms[0]->name; /** @noinspection PhpUndefinedFunctionInspection */ $result->recipe->cook_time = $meta['cook_time'][0] ? recipress_time($meta['cook_time'][0], 'iso') : ''; /** @noinspection PhpUndefinedFunctionInspection */ $result->recipe->prep_time = $meta['prep_time'][0] ? recipress_time($meta['prep_time'][0], 'iso') : ''; $result->recipe->yield = $meta['yield'][0] ? $meta['yield'][0] : ''; $result->recipe->serves = $meta['servings'][0] ? $meta['servings'][0] : ''; $ingredients = $meta['ingredient']; $ingredients = unserialize($ingredients[0]); foreach ($ingredients as $ingredient) { $newIngredient = $ingredient['amount'] . ' ' . $ingredient['measurement'] . ' ' . $ingredient['ingredient']; if (!empty($ingredient['notes'])) { $newIngredient .= ', ' . $ingredient['notes']; } $result->ingredients[] = trim(str_replace(' ', ' ', $newIngredient)); } $instructions = $meta['instruction']; $instructions = unserialize($instructions[0]); foreach ($instructions as $instruction) { $result->recipe->instructions .= $instruction['description']; if (!empty($instruction['image'])) { $result->recipe->instructions .= "[br]" . wp_get_attachment_image($instruction['image'], $size, false); $result->recipe->instructions = str_replace('<', '[', $result->recipe->instructions); $result->recipe->instructions = str_replace('>', ']', $result->recipe->instructions); } $result->recipe->instructions .= "\n"; } break; /** * Make the Recipe Card data look like RecipeSEO/Ziplist - only because we already have the JS for those */ /** * Make the Recipe Card data look like RecipeSEO/Ziplist - only because we already have the JS for those */ case 'yumprint': $post = $wpdb->get_row("SELECT * FROM " . $wpdb->prefix . "yumprint_recipe_recipe WHERE id=" . $postID); $nutrition = @json_decode($post->nutrition); $recipe = @json_decode($post->recipe); if (empty($recipe)) { $recipe = new stdClass(); } $recipe->recipe_title = !empty($recipe->title) ? $recipe->title : ''; $recipe->recipe_image = !empty($recipe->image) ? $recipe->image : ''; $ingredients = !empty($recipe->ingredients) && is_array($recipe->ingredients) ? $recipe->ingredients : array(); $result->ingredients = array(); foreach ($ingredients as $value) { if ($value->title != '') { $result->ingredients[] = '!' . $value->title; } foreach ($value->lines as $line) { $result->ingredients[] = $line; } } $instructions = !empty($recipe->directions) && is_array($recipe->directions) ? $recipe->directions : array(); $recipe->instructions = array(); foreach ($instructions as $value) { if ($value->title != '') { $recipe->instructions[] = '!' . $value->title; } foreach ($value->lines as $line) { $recipe->instructions[] = $line; } } $recipe->instructions = implode("\n", $recipe->instructions); $recipe->notes = !empty($recipe->notes) && is_array($recipe->notes) ? implode("\n", $recipe->notes[0]->lines) : ''; $recipe->prep_time = !empty($recipe->prepTime) ? $this->yumprintTime($recipe->prepTime) : ''; $recipe->cook_time = !empty($recipe->cookTime) ? $this->yumprintTime($recipe->cookTime) : ''; $recipe->yield = !empty($recipe->yields) ? $recipe->yields : ''; $recipe->serving_size = !empty($recipe->servings) ? $recipe->servings : ''; $serves = !empty($recipe->servings) ? (int) $recipe->servings : ''; $div = !empty($serves) ? $serves : 1; if (!empty($nutrition)) { /** @noinspection PhpUnusedLocalVariableInspection */ foreach ($nutrition as $key => &$value) { $value /= $div; } $nutrition->calories = round($nutrition->calories); $nutrition->totalFat = round($nutrition->totalFat) . 'g'; $nutrition->saturatedFat = round($nutrition->saturatedFat) . 'g'; $nutrition->transFat = round($nutrition->transFat) . 'g'; $nutrition->polyunsaturatedFat = round($nutrition->polyunsaturatedFat); $nutrition->monounsaturatedFat = round($nutrition->monounsaturatedFat); $nutrition->unsaturatedFat = $nutrition->polyunsaturatedFat + $nutrition->monounsaturatedFat . 'g'; $nutrition->cholesterol = round($nutrition->cholesterol) . 'mg'; $nutrition->sodium = round($nutrition->sodium) . 'mg'; $nutrition->totalCarbohydrates = round($nutrition->totalCarbohydrates) . 'g'; $nutrition->dietaryFiber = round($nutrition->dietaryFiber) . 'g'; $nutrition->sugars = round($nutrition->sugars) . 'g'; $nutrition->protein = round($nutrition->protein) . 'g'; foreach ($nutrition as $key => &$value) { if ($value == '0g' || $value == '0mg') { $value = ''; } } } else { $nutrition = new stdClass(); $nutrition->calories = ''; $nutrition->totalFat = ''; $nutrition->saturatedFat = ''; $nutrition->transFat = ''; $nutrition->polyunsaturatedFat = ''; $nutrition->monounsaturatedFat = ''; $nutrition->unsaturatedFat = ''; $nutrition->cholesterol = ''; $nutrition->sodium = ''; $nutrition->totalCarbohydrates = ''; $nutrition->dietaryFiber = ''; $nutrition->sugars = ''; $nutrition->protein = ''; } $result->recipe = $recipe; $result->recipe->nutrition = $nutrition; unset($result->recipe->prepTime); unset($result->recipe->cookTime); unset($result->recipe->totalTime); unset($result->recipe->yields); unset($result->recipe->servings); unset($result->recipe->title); unset($result->recipe->image); unset($result->recipe->ingredients); unset($result->recipe->directions); break; /** * Get me cooking */ /** * Get me cooking */ case 'gmc': case 'gmc_recipe': /** * If GMC is installed, use it to get ingredients, but turn off error reporting to stop it crashing */ if (function_exists('print_ingredient_description')) { error_reporting(0); $gmcInstalled = true; } else { $gmcInstalled = false; } $result->recipe = new stdClass(); $post = get_post($postID); $result->recipe->recipe_title = html_entity_decode(get_the_title($postID), ENT_COMPAT, 'UTF-8'); $result->recipe->author = get_post_meta($postID, 'gmc-source-name', true); $result->recipe->summary = get_post_meta($post->ID, "gmc-description", true); $thumbID = get_post_thumbnail_id($postID); $postThumb = $thumbID != 0 ? wp_get_attachment_image_src($thumbID, 'medium') : ''; if (!empty($postThumb)) { $result->recipe->recipe_image = $postThumb[0]; } $prepHour = (int) get_post_meta($postID, "gmc-prep-time-hours", true); $prepMinute = (int) get_post_meta($postID, "gmc-prep-time-mins", true); $cookHour = (int) get_post_meta($postID, "gmc-cooking-time-hours", true); $cookMinute = (int) get_post_meta($postID, "gmc-cooking-time-mins", true); $result->recipe->prep_time = "PT{$prepHour}H{$prepMinute}M"; $result->recipe->cook_time = "PT{$cookHour}H{$cookMinute}M"; $mealTypes = wp_get_object_terms($postID, 'gmc_course'); if ($mealTypes instanceof WP_Error) { register_taxonomy('gmc_course', 'gmc_recipe'); $mealTypes = wp_get_object_terms($postID, 'gmc_course'); } if (is_array($mealTypes) && count($mealTypes) > 0) { $result->recipe->mealType = $mealTypes[0]->name; } $regions = wp_get_object_terms($postID, 'gmc_region'); if ($regions instanceof WP_Error) { register_taxonomy('gmc_region', 'gmc_recipe'); $regions = wp_get_object_terms($postID, 'gmc_region'); } if (is_array($regions) && count($regions) > 0) { $result->recipe->cuisine = $regions[0]->name; } $result->ingredients = array(); $result->recipe->instructions = ''; $currentSection = ''; $steps = get_posts('post_status=publish&post_type=gmc_recipestep&nopaging=1&orderby=menu_order&order=ASC&post_parent=' . $postID); /** @var $step WP_Post */ foreach ($steps as $step) { if (!empty($step->post_content)) { $section = get_post_meta($step->ID, 'gmc_stepgroup', true); if (!empty($section) && $section != $currentSection) { $result->recipe->instructions .= "!{$section}\n"; $currentSection = $section; } $content = preg_replace('/\\r?\\n/i', '[br]', $step->post_content); $thumbID = get_post_thumbnail_id($step->ID); if ($thumbID) { $postThumb = wp_get_attachment_image_src($thumbID, 'medium'); if ($postThumb) { $content .= '[br][img src="' . $postThumb[0] . '" width="' . $postThumb[1] . '" height="' . $postThumb[2] . '"]'; } } $result->recipe->instructions .= $content . "\n"; } } $result->recipe->instructions = rtrim($result->recipe->instructions, "\n"); $ingredients = get_posts('post_status=publish&post_type=gmc_recipeingredient&nopaging=1&orderby=menu_order&order=ASC&post_parent=' . $postID); $currentSection = ''; /** @var $ingredient WP_Post */ foreach ($ingredients as $ingredient) { $section = get_post_meta($ingredient->ID, "gmc-ingredientgroup", true); if (!empty($section) && $section != $currentSection) { $result->ingredients[] = "!{$section}"; $currentSection = $section; } if ($gmcInstalled) { /** @noinspection PhpUndefinedFunctionInspection */ $iLine = trim(print_ingredient_description($ingredient)); } else { $quantity = get_post_meta($ingredient->ID, "gmc-ingredientquantity", true); $measurement = get_post_meta($ingredient->ID, 'gmc-ingredientmeasurement', true); $title = $ingredient->post_title; $iLine = trim("{$quantity} {$measurement} {$title}"); } if (!empty($iLine)) { $result->ingredients[] = $iLine; } } /** * Try to convert HTML in notes into something EasyRecipe will understand */ $notes = preg_replace_callback('%<(strong|em)>(.*?)</\\1>%', array($this, 'notesConversion'), $post->post_content); $notes = preg_replace('%<a ([^>]+)>(.*?)</a>%i', '[url $1]$2[/url]', $notes); $result->recipe->notes = strip_tags($notes); $result->recipe->yield = $servings = get_post_meta($post->ID, "gmc-nr-servings", true); $nutrition = new stdClass(); if (get_post_meta($postID, "gmc_has_nutrition", true)) { $result->recipe->serving_size = get_post_meta($post->ID, "gmc_gda_servings", true); $nutrition->calories = get_post_meta($post->ID, "gmc_nutrition_kcal_serving", true); if ($nutrition->calories != '') { $nutrition->calories *= 1000; } $nutrition->totalFat = round(get_post_meta($post->ID, "gmc_nutrition_fat_total_serving", true)) . 'g'; $nutrition->saturatedFat = round(get_post_meta($post->ID, "gmc_nutrition_fat_sat_serving", true)) . 'g'; $nutrition->sodium = round(get_post_meta($post->ID, "gmc_nutrition_salt_sod_serving", true)) . 'mg'; $nutrition->totalCarbohydrates = round(get_post_meta($post->ID, "gmc_nutrition_carb_total_serving", true)) . 'g'; $nutrition->dietaryFiber = round(get_post_meta($post->ID, "gmc_nutrition_fibre_serving", true)) . 'g'; $nutrition->sugars = round(get_post_meta($post->ID, "gmc_nutrition_carb_sugar_serving", true)) . 'g'; $nutrition->protein = round(get_post_meta($post->ID, "gmc_nutrition_protein_serving", true)) . 'g'; } $result->recipe->nutrition = $nutrition; break; case 'kitchebug': break; } return $result; }
/** * Mastercook can produce concatenated XML files, and they may possibly have different encoding * So we need to split the input into separate XML chunks and process each separately */ private function mastercook() { $content = @file_get_contents($this->filePath); /** * Clean up whitespace */ $content = str_replace("\r", "\n", $content); $content = preg_replace('/\\n\\n+/', "\n", $content); $content = trim($content); $xml = array(); $nFiles = 1; $xml[0] = -1; while (($xml[] = strpos($content, '<?xml', $xml[$nFiles - 1] + 1)) !== false) { $nFiles++; } if ($nFiles < 2) { $this->result->error = "There are no recipes in this file"; return; } /** * Get each XML chunk */ $files = array(); for ($i = 1; $i < $nFiles; $i++) { $length = $xml[$i + 1] ? $xml[$i + 1] - $xml[$i] : strlen($content) - $xml[$i]; $files[] = substr($content, $xml[$i], $length); } $recipes = array(); /** * Process each xml chunk separately */ while (count($files) > 0) { $content = array_pop($files); /** * Get the encoding and remove the original XML tag * Not all mx2 files have an encoding specified - assume UTF-8 if none */ $endXML = strpos($content, '?>') + 2; $xmlString = substr($content, 0, $endXML); $content = substr($content, $endXML); if (preg_match('/(?:encoding="([^"]+)").*/i', $xmlString, $regs)) { $encoding = strtoupper($regs[1]); } else { $encoding = 'UTF-8'; } /** * Make a new XML tag that DOMDocument will accept * DOMDocument doesn't recognise some attributes that may be present in the original (e.g. standalone) */ $xmlString = "<?xml version=\"1.0\" encoding=\"{$encoding}\"?>"; $dom = new EasyRecipePlusDOMDocument("", false); if (!@$dom->loadXML($xmlString . $content)) { $error = libxml_get_last_error(); $line = $error ? $error->line : 0; $msg = $error ? $error->message : "Unknown error"; $this->result->error = "Error on line {$line}: {$msg}"; continue; } $recipeElements = $dom->getElementsByTagName("RcpE"); if ($recipeElements->length == 0) { $this->result->error = "There are no recipes in this file"; continue; } foreach ($recipeElements as $recipe) { $recipes[] = $this->parseMastercookRecipe($recipe); } /** * Try to salvage some memory - this process is very memory intensive */ $content = null; $recipeElements = null; $dom = null; } $this->writeTemp($recipes); }
/** * Remove non display stuff * * @param $content * * @return mixed */ function filterExcerpt($content) { $dom = new EasyRecipePlusDOMDocument($content); $dom->removeElementsByClassName('ERSSavePrint', 'div'); $dom->removeElementsByClassName('ERSRating', 'div'); $dom->removeElementsByClassName('ERSRatings', 'div'); $dom->removeElementsByClassName('ERSClear', 'div'); $dom->removeElementsByClassName('endeasyrecipe', 'div'); $dom->removeElementsByClassName('ERSLinkback', 'div'); $content = $dom->getHTML(true); /** * Remove empty lines left over from the deletions */ return preg_replace('/(\\r\\n|\\n)(?:\\r\\n|\\n)+/', '$1', $content); }