/**
  * 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;
 }
 /**
  * Get the post, extract the recipe and combine with the current style and output it
  *
  * @param integer $postID The post ID to print
  * @param integer $recipeIX The zero based index of the recipe in the post
  */
 public function printRecipe($postID, $recipeIX)
 {
     /** @var $wpdb wpdb */
     global $wpdb;
     $settings = EasyRecipePlusSettings::getInstance();
     /**
      * Be paranoid and force the ID to an integer
      */
     $postID = (int) $postID;
     $q = "SELECT * FROM {$wpdb->posts} WHERE ID = {$postID}";
     $post = $wpdb->get_row($q);
     if (!$post) {
         return;
     }
     /**
      * Process the [br] shortcodes and remove the spurious <br>'s that wp_auto() inserts
      */
     $content = str_replace("[br]", "<br>", $post->post_content);
     $content = preg_replace('%</div>\\s*</p></div>%im', '</div></div>', $content);
     $content = $this->plugin->possiblyConvert($postID, $post->post_type, $content);
     $postDOM = new EasyRecipePlusDocument($content);
     if (!$postDOM->isEasyRecipe) {
         return;
     }
     /**
      * If the post is formatted already then it came from the Object cache (?)
      * If that's the case we need to re-read the original
      */
     if ($postDOM->isFormatted) {
         $post = $wpdb->get_row("SELECT * FROM " . $wpdb->prefix . "posts WHERE ID = {$postID}");
         $content = str_replace("[br]", "<br>", $post->post_content);
         $content = preg_replace('%</div>\\s*</p></div>%im', '</div></div>', $content);
         $content = $this->plugin->possiblyConvert($postID, '', $content);
         $postDOM = new EasyRecipePlusDocument($content);
         if (!$postDOM->isEasyRecipe) {
             return;
         }
     }
     if (isset($_GET['style'])) {
         $styleName = $_GET['style'];
     } else {
         $styleName = $settings->printStyle;
     }
     //        $printStyleData = call_user_func(array($this->stylesClass, 'getStyleData'), $styleName, $settings->get('customTemplates'), true);
     $printStyleData = EasyRecipePlusStyles::getStyleData($styleName, $settings->customTemplates, true);
     if (get_locale() != 'en_US') {
         EasyRecipePlusTemplate::setTranslate('easyrecipe');
     }
     /**
      * Fix possibly broken times in older posts
      * Fix the Cholesterol oops in early versions
      */
     if ($postDOM->recipeVersion < '3') {
         $postDOM->fixTimes("preptime");
         $postDOM->fixTimes("cooktime");
         $postDOM->fixTimes("duration");
         $postDOM->setParentValueByClassName("cholestrol", $settings->lblCholesterol, "Cholestrol");
     }
     $postDOM->setSettings($settings);
     $data = new stdClass();
     $data->hasRating = false;
     $data->convertFractions = $settings->convertFractions;
     $settings->getLabels($data);
     $data->hasLinkback = $settings->allowLink;
     $data->title = $post->post_title;
     $data->blogname = get_option("blogname");
     $data->recipeurl = get_permalink($post->ID);
     $data->customCSS = $this->plugin->getCSS('Print');
     $data->extraPrintHeader = $settings->extraPrintHeader;
     $data->easyrecipeURL = EasyRecipePlus::$EasyRecipePlusUrl;
     $recipe = $postDOM->getRecipe($recipeIX);
     $photoURL = $postDOM->findPhotoURL($recipe);
     $data->hasPhoto = !empty($photoURL);
     $data->jqueryjs = self::JQUERYJS;
     $data->jqueryuijs = self::JQUERYUIJS;
     $data->jqueryuicss = self::JQUERYUICSS;
     if (current_user_can('edit_posts')) {
         $data->isAdmin = true;
         $data->formatDialog = $this->plugin->getFormatDialog($printStyleData, true);
         $cssLink = '<link href="' . EasyRecipePlus::$EasyRecipePlusUrl . '/css/%s?version=' . EasyRecipePlus::$pluginVersion . '" rel="stylesheet" type="text/css"/>';
         $jsLink = '<script type="text/javascript" src="' . EasyRecipePlus::$EasyRecipePlusUrl . '/js/%s?version=' . EasyRecipePlus::$pluginVersion . '"></script>';
         $data->formatCSS = sprintf($cssLink, 'easyrecipe-format-min.css');
         $data->formatJS = sprintf($jsLink, 'easyrecipe-format-min.js');
     } else {
         $data->formatDialog = '';
         $data->printJS = '<script type="text/javascript" src="' . EasyRecipePlus::$EasyRecipePlusUrl . '/js/easyrecipe-print-min.js?version=' . EasyRecipePlus::$pluginVersion . '"></script>';
     }
     $data->style = $styleName;
     if ($data->style[0] == '_') {
         $style = substr($data->style, 1);
         $data->css = "/easyrecipe-printstyle";
         $templateFile = $settings->customTemplates . "/printstyles/{$style}/style.html";
     } else {
         $data->css = EasyRecipePlus::$EasyRecipePlusUrl . "/printstyles/{$data->style}";
         $templateFile = EasyRecipePlus::$EasyRecipePlusDir . "/printstyles/{$data->style}/style.html";
     }
     $data->css .= "/style.css?version=" . EasyRecipePlus::$pluginVersion . ".{$printStyleData->version}";
     $template = new EasyRecipePlusTemplate($templateFile);
     /**
      * Brain dead IE shows "friendly" error pages (i.e. it's non-compliant) so we need to force a 200
      */
     header("HTTP/1.1 200 OK");
     /**
      * Set the character encoding explicitly
      */
     $charset = get_bloginfo('charset');
     header("Content-Type:text/html; charset={$charset}");
     echo $postDOM->formatRecipe($recipe, $template, $data);
     flush();
     exit;
 }
 /**
  * @param WP_Post $post
  * @param bool    $updateAll TRUE if the update is part of updateAll() - if so, defer the term count
  */
 public function update($post, $updateAll = false)
 {
     /** @var wpdb $wpdb */
     global $wpdb;
     $content = $post->post_content;
     /**
      * Do a quick check that there IS a recipe before we go to the expense of instantiating a DOMDocument
      */
     if (strpos($content, 'endeasyrecipe') === false) {
         return;
     }
     $dom = new EasyRecipePlusDocument($content);
     if (!$dom->isEasyRecipe) {
         return;
     }
     $cuisineTerms = array();
     $courseTerms = array();
     $postID = $post->ID;
     if (!$updateAll) {
         $this->countTerms['cuisine'] = array();
         $this->countTerms['course'] = array();
     }
     /**
      * Read all the current cuisine/course terms for this post
      * We need this to decide if we need to insert a new relationship and whether we need to remove any old relationships no longer used
      */
     $q = "SELECT {$wpdb->term_taxonomy}.term_taxonomy_id, taxonomy FROM {$wpdb->terms} JOIN {$wpdb->term_taxonomy} ON {$wpdb->terms}.term_id = {$wpdb->term_taxonomy}.term_id JOIN ";
     $q .= "{$wpdb->term_relationships} on {$wpdb->term_relationships}.term_taxonomy_id = {$wpdb->term_taxonomy}.term_taxonomy_id ";
     $q .= "WHERE object_id = {$postID} AND taxonomy in ('course','cuisine')";
     $existing = $wpdb->get_results($q);
     $existingUnused = array();
     foreach ($existing as $exist) {
         $existingUnused[$exist->term_taxonomy_id] = true;
         $this->countTerms[$exist->taxonomy][] = $exist->term_taxonomy_id;
     }
     /**
      * Get the course(s) and cuisine(s) from the recipe
      */
     $cuisines = $dom->getElementsByClassName('cuisine');
     $courses = $dom->getElementsByClassName('type');
     if (count($cuisines) > 0) {
         /** @var DOMElement $cuisine */
         foreach ($cuisines as $cuisine) {
             $term = $cuisine->nodeValue;
             /**
              * Get the term info for the recipe's cuisine(s) - insert one of it doesn't yet exist
              */
             if (empty($cuisineTerms[$term])) {
                 $termInfo = term_exists($term, 'cuisine');
                 if (!$termInfo) {
                     $termInfo = wp_insert_term($term, 'cuisine');
                 }
                 $cuisineTerms[$term] = $termInfo['term_taxonomy_id'];
             }
             /**
              * Check to see if we already have the correct relationship for this cuisine term for this post
              */
             $ttID = $cuisineTerms[$term];
             if (!empty($existingUnused[$ttID])) {
                 $existingUnused[$ttID] = false;
                 continue;
             }
             /**
              * If we have multiple recipes in the post, it's possible we have already seen this cuisine and if so, don't insert it a second time
              * Otherwise, the relationship didn't exist so insert it
              */
             if (!in_array($ttID, $this->countTerms['cuisine'])) {
                 $this->countTerms['cuisine'][] = $ttID;
                 $wpdb->insert($wpdb->term_relationships, array('object_id' => $postID, 'term_taxonomy_id' => $ttID));
             }
         }
     }
     if (count($courses) > 0) {
         /** @var DOMElement $course */
         foreach ($courses as $course) {
             $term = $course->nodeValue;
             /**
              * Get the term info for the recipe's course(s) - insert one of it doesn't yet exist
              */
             if (empty($courseTerms[$term])) {
                 $termInfo = term_exists($term, 'course');
                 if (!$termInfo) {
                     $termInfo = wp_insert_term($term, 'course');
                 }
                 $courseTerms[$term] = $termInfo['term_taxonomy_id'];
             }
             /**
              * Check to see if we already have the correct relationship for this course term for this post
              */
             $ttID = $courseTerms[$term];
             if (!empty($existingUnused[$ttID])) {
                 $existingUnused[$ttID] = false;
                 continue;
             }
             /**
              * If we have multiple recipes in the post, it's possible we have already seen this course and if so, don't insert it a second time
              * Otherwise, the relationship didn't exist so insert it
              */
             if (!in_array($ttID, $this->countTerms['course'])) {
                 $this->countTerms['course'][] = $ttID;
                 $wpdb->insert($wpdb->term_relationships, array('object_id' => $postID, 'term_taxonomy_id' => $ttID));
             }
         }
     }
     /**
      * Remove any existing term relationships that are now no longer used and adjust the list of terms that need to be updated
      */
     foreach ($existingUnused as $ttID => $unused) {
         if ($unused) {
             $wpdb->delete($wpdb->term_relationships, array('object_id' => $postID, 'term_taxonomy_id' => $ttID));
         } else {
             if (in_array($ttID, $this->countTerms['course'])) {
             }
         }
     }
     /**
      * Update any term counts that we may have adjusted unless this is part of an updateAll() run
      */
     if (!$updateAll) {
         if (count($this->countTerms['cuisine']) > 0) {
             wp_update_term_count_now(array_unique(array_keys($this->countTerms['cuisine'])), 'cuisine');
         }
         if (count($this->countTerms['course']) > 0) {
             wp_update_term_count_now(array_unique(array_keys($this->countTerms['course'])), 'course');
         }
     }
 }