HEX
Server: LiteSpeed
System: Linux node612.namehero.net 4.18.0-553.121.1.lve.el8.x86_64 #1 SMP Thu Apr 30 16:40:41 UTC 2026 x86_64
User: dfwparty (1186)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/dfwparty/magicalmatty.fun/wp-content/plugins/wp-customer-reviews/wp-customer-reviews-3.php
<?php
/*
 * Plugin Name: WP Customer Reviews
 * Plugin URI: https://wordpress.org/plugins/wp-customer-reviews/
 * Description: Allows your visitors to leave business / product reviews. Testimonials are in Microdata / Microformat and may display star ratings in search results.
 * Version: 3.7.8
 * Author: Aaron Queen
 * Author URI: https://wordpress.org/plugins/wp-customer-reviews/
 * Text Domain: wp-customer-reviews
 * License: MIT
 *
 * Copyright (c) 2026 Aaron Queen
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

class WPCustomerReviews3
{
  var $debug = false;
  var $prefix = "wpcr3";
  var $dashname = "wp-customer-reviews-3";
  var $url = "https://wordpress.org/plugins/wp-customer-reviews/";
  var $support_link = "https://wordpress.org/support/plugin/wp-customer-reviews/";
  var $prolink = "https://wordpress.org/plugins/wp-customer-reviews/";
  var $plugin_info = false;
  var $plugin_version = "0.0.0";
  var $adminClass = false;
  var $proClass = false;
  var $pro = false;
  var $force_active_page = false;
  var $options = [];
  var $options_name = "wpcr3_options";
  var $options_url_slug = "wpcr3_options";
  var $p = "";
  var $all_templates = [
    "frontend_review_holder" => "html",
    "frontend_review_form" => "html",
    "frontend_review_form_text_field" => "html",
    "frontend_review_form_rating_field" => "html",
    "frontend_review_form_review_field" => "html",
    "frontend_review_item" => "html",
    "frontend_review_item_aggregate" => "html",
    "frontend_review_item_reviews" => "html",
    "frontend_review_pagination" => "html",
    "frontend_review_rating_stars" => "html",
  ];
  var $allowedFieldTags = [
    "i" => true,
    "em" => true,
    "b" => true,
    "strong" => true,
  ];
  var $allowedContentTags = [
    "br" => true,
    "p" => true,
    "hr" => true,
    "i" => true,
    "em" => true,
    "b" => true,
    "strong" => true,
    "a" => [
      "href" => [],
      "title" => [],
      "target" => [],
      "rel" => [],
      "style" => [],
      "class" => [],
    ],
    "img" => [
      "src" => [],
      "alt" => [],
      "width" => [],
      "height" => [],
      "style" => [],
      "class" => [],
    ],
  ];

  function __construct() {}

  function start()
  {
    $this->debug =
      isset($_SERVER) &&
      isset($_SERVER["SERVER_NAME"]) &&
      stripos($_SERVER["SERVER_NAME"], "wptest.bomp") !== false;

    if ($this->debug === true || $this->remote_debug()) {
      restore_error_handler();
      error_reporting(E_ALL);
      ini_set("error_reporting", E_ALL);
      ini_set("html_errors", true);
      ini_set("display_errors", true);
    }

    register_activation_hook(__FILE__, [&$this, "activate"]);
    register_deactivation_hook(__FILE__, [&$this, "deactivate"]);

    // we use priority 11 to allow v2 and v3 to coexist
    add_action("init", [&$this, "init"], 11);
  }

  function remote_debug()
  {
    return isset($_GET["wpcr3_debug"]);
  }

  function plugin_get_info()
  {
    include_once ABSPATH . "wp-admin/includes/plugin.php";
    return get_plugin_data(__FILE__);
  }

  function include_goatee()
  {
    if (!defined("HAS_" . $this->prefix . "_GOATEE")) {
      define("HAS_" . $this->prefix . "_GOATEE", 1);
      include_once $this->getplugindir() . "include/goatee-php/wpcr-goatee.php"; // include Goatee templating functions
    }
  }

  function include_pro()
  {
    if (!defined("HAS_" . $this->prefix . "_PRO")) {
      $pro_file =
        $this->getplugindir() .
        "../wp-customer-reviews-pro-activation/wp-customer-reviews-3-pro-inc.php";
      $pro_exists = file_exists($pro_file);
      if ($pro_exists === false) {
        return;
      }

      include_once ABSPATH . "wp-admin/includes/plugin.php";
      if (
        is_plugin_active(
          "wp-customer-reviews-pro-activation/wp-customer-reviews-3-pro.php",
        ) === false
      ) {
        return;
      }

      define("HAS_" . $this->prefix . "_PRO", 1);
      include_once $pro_file; // include pro functions
      $this->proClass = new WPCustomerReviews3Pro();
      $this->proClass->start_pro($this);
    }
  }

  // forward to pro class
  function pro_function()
  {
    if (!$this->pro) {
      die("Pro Function called without pro loaded.");
    }
    $args = func_get_args();
    $function = array_shift($args);
    return call_user_func_array([$this->proClass, $function], $args);
  }

  function include_admin()
  {
    if (!defined("HAS_" . $this->prefix . "_ADMIN")) {
      define("HAS_" . $this->prefix . "_ADMIN", 1);
      include_once $this->getplugindir() .
        "include/admin/wp-customer-reviews-3-admin.php"; // include admin functions
      $this->adminClass = new WPCustomerReviewsAdmin3();
      $this->adminClass->start_admin($this);
    }
  }

  // forward to admin class
  function admin_function()
  {
    $this->include_admin();
    $args = func_get_args();
    $function = array_shift($args);
    return call_user_func_array([$this->adminClass, $function], $args);
  }

  function admin_menu()
  {
    $this->admin_function("real_admin_menu");
  }

  function admin_init()
  {
    $this->admin_function("real_admin_init");
  }

  // if $this->p->$key does not exist, set it to empty string
  function param($keys, &$object = "")
  {
    if (!is_array($keys)) {
      $keys = [$keys];
    }
    if ($object === "") {
      $object = $this->p;
    }
    foreach ($keys as $key) {
      if (is_object($object)) {
        if (!isset($object->$key)) {
          $object->$key = "";
        }
      } elseif (is_array($object)) {
        if (!array_key_exists($key, $object)) {
          $object[$key] = "";
        }
      }
    }
  }

  function get_options()
  {
    $this->options = get_option($this->options_name);
  }

  function sanitize_p_obj_array($valArr)
  {
    foreach ($valArr as $k => $v) {
      if (is_array($v)) {
        $valArr[$k] = $this->sanitize_p_obj_array($v);
        continue;
      }

      $valArr[$k] = trim(stripslashes($v));
    }

    return $valArr;
  }

  function sanitize_p_obj()
  {
    foreach ($this->p as $c => $val) {
      if (is_array($val)) {
        $this->p->$c = $this->sanitize_p_obj_array($val);
        continue;
      }

      // this is escaped on output by wp_kses() using allowedContentTags or allowedFieldTags, depending on the context
      $this->p->$c = trim(stripslashes($val));
    }
  }

  function make_p_obj()
  {
    $this->p = new stdClass();

    if (is_admin()) {
      // $_GET is used mainly by filters for admin pages, but no intended use case for this in frontend
      foreach ($_GET as $c => $val) {
        $this->p->$c = $val;
      }
    }

    foreach ($_POST as $c => $val) {
      $this->p->$c = $val;
    }

    $this->sanitize_p_obj();
  }

  function is_active_page()
  {
    global $post;

    // if using WPCR_INSERT, we always force the page active so reviews will output
    if ($this->force_active_page === "shortcode_insert") {
      return $this->force_active_page;
    }

    if ($this->remote_debug()) {
      $debug = "\n <div style='display:none;'>";
      $debug .= "\n wpcr3_info is_active_page() post={$post->ID} is_active_page=";
      $debug .= "" . print_r(is_singular(), true);
      $debug .= "," . print_r(is_single(), true);
      $debug .= "," . print_r(is_page(), true);
      $debug .= "\n </div>";
      print $debug;
    }

    // not on a single post/page, do not output
    if (!is_singular()) {
      return 0;
    }

    $enabled_post = get_post_meta($post->ID, "wpcr3_enable", true);
    if ($enabled_post == "1") {
      return "enabled";
    }

    return 0;
  }

  // run after each shortcode has finished
  function reset_active_page()
  {
    $this->force_active_page = false;
  }

  function rand_string($length)
  {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $str = "";

    $size = strlen($chars);
    for ($i = 0; $i < $length; $i++) {
      $str .= $chars[rand(0, $size - 1)];
    }

    return $str;
  }

  function get_aggregate_reviews($postid)
  {
    global $wpdb;

    /*
		Removed below INNER JOIN for 3.2.2 to allow in-text shortcode (and do_shortcode) to display correct review count even when WPCR-enabled checkbox is not checked on the page. (loc 1/2)
		... FROM {$wpdb->prefix}posts p1 ...
		>> INNER JOIN {$wpdb->prefix}postmeta pm1 ON pm1.meta_key = 'wpcr3_enable' AND pm1.meta_value = '1' AND pm1.post_id = p1.id
		... pm2 ON pm2.meta_key ...
		*/

    $query = $wpdb->prepare(
      "
			SELECT
			COUNT(*) AS aggregate_count, AVG(tmp2.rating) AS aggregate_rating
			FROM (
				SELECT pm4.meta_value AS rating
				FROM (
					SELECT DISTINCT pm2.post_id
					FROM {$wpdb->prefix}posts p1
					INNER JOIN {$wpdb->prefix}postmeta pm2 ON pm2.meta_key = 'wpcr3_review_post' AND pm2.meta_value = p1.id
					WHERE p1.id = %d
				) tmp1
				INNER JOIN {$wpdb->prefix}posts p2 ON p2.id = tmp1.post_id AND p2.post_status = 'publish' AND p2.post_type = 'wpcr3_review'
				INNER JOIN {$wpdb->prefix}postmeta pm4 ON pm4.post_id = p2.id AND pm4.meta_key = 'wpcr3_review_rating' AND pm4.meta_value IS NOT NULL AND pm4.meta_value != '0'
				GROUP BY p2.id
			) tmp2
		",
      intval($postid),
    );

    $results = $wpdb->get_results($query);

    $rtn = new stdClass();

    if (count($results)) {
      $rtn->aggregate_count = intval($results[0]->aggregate_count);
      $rtn->aggregate_rating =
        $rtn->aggregate_count === 0
          ? 0
          : number_format(floatval($results[0]->aggregate_rating), 2);
      $rtn->aggregate_count_valid = $rtn->aggregate_count > 0;
      $rtn->stars = $this->get_rating_template($rtn->aggregate_rating, false);
    }

    return $rtn;
  }

  // filters show reviews only display for posts that have wpcr enabled on them AND are published
  function reviews_attached_to_enabled_published_posts($where)
  {
    global $wpdb;

    /*
		Removed below INNER JOIN for 3.2.2 to allow in-text shortcode (and do_shortcode) to display correct review count even when WPCR-enabled checkbox is not checked on the page. (loc 2/2)
		... pm2 ON pm2.meta_key ...
		> INNER JOIN {$wpdb->prefix}postmeta pm3 ON pm3.meta_key = 'wpcr3_enable' AND pm3.meta_value = '1'
		... p3 ON p3.id = pm2.meta_value ...
		*/

    $where .= "
			AND {$wpdb->prefix}posts.id IN (
				SELECT DISTINCT p2.id FROM {$wpdb->prefix}posts p2
				INNER JOIN {$wpdb->prefix}postmeta pm2 ON pm2.meta_key = 'wpcr3_review_post' AND pm2.post_id = p2.id
				INNER JOIN {$wpdb->prefix}posts p3 ON p3.id = pm2.meta_value AND p3.post_status = 'publish'
				WHERE p2.post_type = 'wpcr3_review'
			)
		";
    return $where;
  }

  function get_reviews($postid, $thispage, $opts)
  {
    $queryOpts = [
      "orderby" => "date",
      "order" => "DESC",
      "showposts" => min($opts->perpage, $opts->num),
      "post_type" => $this->prefix . "_review",
      "post_status" => "publish",
      "paged" => $thispage,
    ];

    if ($postid != -1) {
      // if $postid is not -1 (all reviews from all posts), need to filter by meta value for post id
      $meta_query = ["relation" => "AND"];
      $meta_query[] = [
        "key" => "{$this->prefix}_review_post",
        "value" => $postid,
        "compare" => "=",
      ];
      $queryOpts["meta_query"] = $meta_query;
    }

    add_filter("posts_where", [
      &$this,
      "reviews_attached_to_enabled_published_posts",
    ]);
    $reviews = new WP_Query($queryOpts);
    remove_filter("posts_where", [
      &$this,
      "reviews_attached_to_enabled_published_posts",
    ]);

    $rtn = new stdClass();
    $rtn->reviews = [];
    $rtn->found_posts = $reviews->found_posts;

    foreach ($reviews->posts as $post) {
      $review = $this->get_post_custom_single($post->ID);

      $review["id"] = $post->ID;

      $params = [
        $this->prefix . "_review_name",
        $this->prefix . "_review_title",
        $this->prefix . "_review_website",
        $this->prefix . "_review_admin_response",
      ];
      $this->param($params, $review);

      if (
        $this->options["standard_fields"]["fname"]["show"] == 0 ||
        $review[$this->prefix . "_review_name"] == ""
      ) {
        $review[$this->prefix . "_review_name"] = "Anonymous";
      }

      if ($this->options["standard_fields"]["ftitle"]["show"] == 0) {
        $review[$this->prefix . "_review_title"] = "";
      }

      if ($this->options["standard_fields"]["fwebsite"]["show"] == 0) {
        $review[$this->prefix . "_review_website"] = "";
      }

      $review[$this->prefix . "_custom_fields"] = [];

      if ($opts->hidecustom == 0) {
        foreach ($this->options["custom_fields"] as $name => $fieldArr) {
          $params = [$this->prefix . "_" . $name];
          $this->param($params, $review);
          $value = trim($review[$this->prefix . "_" . $name]);
          if ($fieldArr["show"] == 1 && strlen($value)) {
            $review[$this->prefix . "_custom_fields"][] = [
              "label" => $fieldArr["label"],
              "value" => $value,
            ];
          }
        }
      }

      $review[$this->prefix . "_review_admin_response"] = nl2br(
        $review[$this->prefix . "_review_admin_response"],
      );

      if ($opts->hideresponse == 1) {
        unset($review[$this->prefix . "_review_admin_response"]);
      }

      // BEG: xss protect
      foreach ($review as $k => $r) {
        if ($k === $this->prefix . "_custom_fields" || $k === "content") {
          continue;
        }

        $review[$k] = wp_kses($r, $this->allowedFieldTags);
      }

      foreach ($review[$this->prefix . "_custom_fields"] as $k => $r) {
        $review[$this->prefix . "_custom_fields"][$k]["value"] = wp_kses(
          $r["value"],
          $this->allowedFieldTags,
        );
      }

      $review["content"] = wp_kses(
        $review["content"],
        $this->allowedContentTags,
      );
      // END: xss protect

      $review["stars"] = $this->get_rating_template(
        $review[$this->prefix . "_review_rating"],
        false,
      );

      $rtn->reviews[] = $review;
    }

    return $rtn;
  }

  function iso8601($time = false)
  {
    if ($time === false) {
      $time = time();
    }
    $date = date("Y-m-d\TH:i:sO", $time);
    return substr($date, 0, strlen($date) - 2) . ":" . substr($date, -2);
  }

  function pagination($opts, $thispage, $total_results)
  {
    $rtn = false;
    $thispage = intval($thispage);
    if ($thispage == 0) {
      $thispage = 1;
    }
    $pages = intval(ceil($total_results / $opts->perpage));

    if ($pages > 1) {
      $rtn = [];
      $rtn["pageOpts"] = htmlspecialchars(json_encode($opts), ENT_QUOTES);
      $rtn["onPage"] = $thispage;
      $rtn["numPages"] = $pages;
      $rtn["pages"] = [];
      $rtn["prevPage"] = max(1, $thispage - 1);
      $rtn["nextPage"] = min($pages, $thispage + 1);

      $range = 2;
      $showitems = $range * 2 + 1;

      $rtn["hasPrev"] = false;
      if ($thispage !== 1) {
        $rtn["hasPrev"] = true;
      }

      for ($i = 1; $i <= $pages; $i++) {
        if ($i === $thispage) {
          $tmp = [];
          $tmp["pageNum"] = $i;
          $tmp["current"] = true;
          $rtn["pages"][] = $tmp;
        } elseif (
          !($i >= $thispage + $range + 1 || $i <= $thispage - $range - 1) ||
          $pages <= $showitems
        ) {
          $tmp = [];
          $tmp["pageNum"] = $i;
          $tmp["current"] = false;
          $rtn["pages"][] = $tmp;
        }
      }

      $rtn["hasNext"] = false;
      if ($thispage !== $pages) {
        $rtn["hasNext"] = true;
      }
    }

    return $rtn;
  }

  function get_meta_or_default($metaArr, $key, $default)
  {
    return isset($metaArr[$key]) ? $metaArr[$key] : $default;
  }

  function get_post_custom_single($postid)
  {
    $post = get_post($postid);
    $meta = get_post_custom($postid);

    $out = [];
    foreach ($meta as $key => $valArr) {
      if ($key === "") {
        continue;
      }

      // escape output for both meta tags and html output
      if ($key === "wpcr3_business_url") {
        $out[$key] = esc_url($valArr[0]);
      } else {
        $out[$key] = esc_attr($valArr[0]);
      }
    }

    if ($post->post_type !== $this->prefix . "_review") {
      $format = $this->get_meta_or_default(
        $out,
        $this->prefix . "_format",
        "Blank Format",
      );
      if ($format === "business") {
        $out["is_business"] = true;
      } elseif ($format === "product") {
        $out["is_product"] = true;
      }
    }

    $out["content"] = nl2br($post->post_content);

    $post_date = explode(" ", $post->post_date);
    $out["post_date"] = $post_date[0];
    $out["post_date"] = date("M j, Y", strtotime($out["post_date"]));

    return $out;
  }

  function inject_parent_info($reviews, $postid, $parentData, $opts)
  {
    $format = $this->get_meta_or_default(
      $parentData,
      $this->prefix . "_format",
      "Blank Format",
    );
    $business_name = $parentData[$this->prefix . "_business_name"];
    $product_name = $parentData[$this->prefix . "_product_name"];
    $postLink = get_permalink($postid);

    foreach ($reviews as &$review) {
      if ($format === "business") {
        $review["is_business"] = true;
        $review["item_name"] = $business_name;
      } elseif ($format === "product") {
        $review["is_product"] = true;
        $review["item_name"] = $product_name;
      }
      $review["postLink"] = $postLink;
      $review["on_same_page"] = $opts->on_postid == $postid;

      if ($opts->snippet > 0) {
        $review["content"] = $this->trim_text_to_word($review, $opts);
      }
    }

    return $reviews;
  }

  function default_parentData($parentData)
  {
    // default some fields for those too lazy to use the plugin properly
    $blog_name = get_bloginfo("name");
    $blog_url = get_bloginfo("url");

    $business_name = $this->get_meta_or_default(
      $parentData,
      $this->prefix . "_business_name",
      $blog_name,
    );
    $product_name = $this->get_meta_or_default(
      $parentData,
      $this->prefix . "_product_name",
      $blog_name,
    );
    $parentData[$this->prefix . "_business_name"] = wp_kses(
      $business_name,
      $this->allowedFieldTags,
    );
    $parentData[$this->prefix . "_product_name"] = wp_kses(
      $product_name,
      $this->allowedFieldTags,
    );
    $parentData[$this->prefix . "_business_url"] = $blog_url;

    // todo: replace with provided image in future
    $parentData[$this->prefix . "_business_image"] =
      $this->getpluginurl_abs() . "css/1x1.png";
    $parentData[$this->prefix . "_product_image"] =
      $this->getpluginurl_abs() . "css/1x1.png";

    return $parentData;
  }

  function output_reviews_show($opts)
  {
    global $post;

    // required: showform, num, postid, classes, showsupport
    // optional: hidecustom, hideresponse, snippet, morelink, thispage, ajax, hidereviews, on_postid (internal), wrapper (internal)

    $params = ["morelink", "classes", "on_postid", "wrapper"];
    $this->param($params, $opts);

    $intArr = [
      "postid",
      "num",
      "paginate",
      "perpage",
      "hidecustom",
      "snippet",
      "showform",
      "hidereviews",
      "hideresponse",
      "ajax",
      "thispage",
      "on_postid",
      "wrapper",
    ];
    foreach ($intArr as $key) {
      $opts->$key = isset($opts->$key)
        ? $this->strip_trim_intval($opts->$key)
        : 0;
    }

    if ($opts->num < 1) {
      $opts->num = 9999;
    }
    if ($opts->thispage < 1) {
      $opts->thispage = 1;
    }
    if ($opts->perpage < 1) {
      $opts->perpage = intval($this->options["reviews_per_page"]);
    }
    if ($opts->perpage < 1) {
      $opts->perpage = 10;
    }

    $postid = $opts->postid;
    $thispage = $opts->thispage;

    if ($opts->num > 0 && $opts->num < $opts->perpage) {
      $opts->perpage = $opts->num;
    }

    if ($opts->ajax == 1) {
      $opts->showform = 0;
    }

    $this->include_goatee();

    $tmp = $this->get_reviews($postid, $thispage, $opts);
    $found_posts = $tmp->found_posts;
    $reviews = $tmp->reviews;
    $page_count = count($reviews);

    $ajaxurl_arr = json_encode(
      explode(".", str_replace("/", "|", $this->getAjaxURL())),
    );

    $on_postid = $opts->on_postid;
    if (isset($post)) {
      $opts->on_postid = $post->ID;
      $on_postid = $opts->on_postid;
    }

    $main_data = [
      "classes" => $opts->classes,
      "review_form" => "",
      "reviews" => "",
      "power" => "",
      "hidereviews" => $opts->hidereviews == 1,
      "ajaxurl" => $ajaxurl_arr,
      "postid" => $postid,
      "on_postid" => $on_postid,
    ];

    $got_post_meta = [];

    $data["found_posts"] = $found_posts;

    $pagination = "";
    if ($opts->hidereviews == 0) {
      if ($found_posts > $opts->perpage && $opts->paginate === 1) {
        $pagination = $this->pagination($opts, $thispage, $found_posts);
        $pagination = wpcr_Goatee::fill(
          $this->options["templates"]["frontend_review_pagination"],
          $pagination,
        );
      }
    }

    if ($opts->showsupport == 1 && $this->options["support_us"] == 1) {
      $main_data["power"] =
        'Powered by <strong><a target="_blank" rel="noopener" href="' .
        $this->url .
        '">' .
        $this->plugin_info["Name"] .
        "</a></strong>";
    }

    $main_data["pagination"] = $pagination;

    if ($postid > 0) {
      // if viewing reviews of a single post id
      $got_post_meta[$postid] = $this->get_post_custom_single($postid);
      $data = &$got_post_meta[$postid];
      $data = $this->default_parentData($data);

      $params = [$this->prefix . "_hideform"];
      $this->param($params, $data);

      $showform =
        $opts->showform == 1 && $data[$this->prefix . "_hideform"] != 1;
      $main_data["review_form"] = $this->show_reviews_form(
        $postid,
        $found_posts,
        $showform,
      );

      $reviews = $this->inject_parent_info($reviews, $postid, $data, $opts);

      $data["postLink"] = get_permalink($postid);
      $data["on_same_page"] = $opts->on_postid == $postid;

      $data["reviews"] = [
        "template" =>
          $this->options["templates"]["frontend_review_item_reviews"],
        "data" => [
          "reviews" => $reviews,
        ],
      ];

      $data["aggregate"] = [
        "template" =>
          $this->options["templates"]["frontend_review_item_aggregate"],
        "data" => [
          "aggregate" => $this->get_aggregate_reviews($postid),
        ],
      ];

      $main_data["reviews"] .= wpcr_Goatee::fill(
        $this->options["templates"]["frontend_review_item"],
        $data,
      );
    } else {
      // we are in a shortcode/widget that is showing reviews for multiple posts
      // so we need to fill $got_post_meta with the post meta related to each $review
      foreach ($reviews as $review) {
        $review_of_postid = intval($review[$this->prefix . "_review_post"]);

        if (!array_key_exists($review_of_postid, $got_post_meta)) {
          $got_post_meta[$review_of_postid] = $this->get_post_custom_single(
            $review_of_postid,
          );
        }

        $data = &$got_post_meta[$review_of_postid];
        $data = $this->default_parentData($data);

        $tmpReviews = [$review];
        $tmpReview = $this->inject_parent_info(
          $tmpReviews,
          $review_of_postid,
          $data,
          $opts,
        );
        $review = $tmpReview[0];

        // 1. no aggregate shown for multiple businesses/products as it would confuse crawlers which one to show in SERPs
        // 2. no form shown for showing "all" reviews because we don't know what post to bind the form to

        if ($opts->hidereviews == 0) {
          $data["reviews"] = [
            "template" =>
              $this->options["templates"]["frontend_review_item_reviews"],
            "data" => [
              "reviews" => [$review],
            ],
          ];
        }

        $main_data["reviews"] .= wpcr_Goatee::fill(
          $this->options["templates"]["frontend_review_item"],
          $data,
        );
      }
    }

    // Useful info: http://wordpress.stackexchange.com/a/39928 ( faster way to get meta values for many posts )
    // Useful info: http://stackoverflow.com/a/18422969 ( outputting multiple ratings for one item )

    $reviews_content = wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_holder"],
      $main_data,
    );
    $reviews_content = preg_replace(
      '/\n\r|\r\n|\n|\r|\t/',
      "",
      $reviews_content,
    ); // minify to prevent automatic line breaks, not removing double spaces

    if ($opts->wrapper === 1) {
      $data_attr = $this->get_data_attr_wrapper($postid);
      $reviews_content = "<div {$data_attr}>" . $reviews_content . "</div>";
    }

    return $reviews_content;
  }

  // stripts html then trims text, but does not break up a word
  function trim_text_to_word($review, $opts)
  {
    $text = $review["content"];
    $text = str_replace("<br", " <br", $text);
    $text = trim(strip_tags($text));
    $len = $opts->snippet;

    if (strlen($text) > $len) {
      preg_match("/^.{0," . $len . "}(?:.*?)\b/siu", $text, $matches);
      $text = $matches[0] . "... ";
      if (strlen(trim($opts->morelink)) > 0) {
        $postLink = $review["postLink"] . "#wpcr3_id_" . $review["id"];
        $text .= "<a href='{$postLink}'>$opts->morelink</a>";
      }
    }

    return $text;
  }

  function print_filters_for($hook = "")
  {
    global $wp_filter;
    if (empty($hook) || !isset($wp_filter[$hook])) {
      return;
    }
    print "<pre>";
    print_r($wp_filter[$hook]);
    print "</pre>";
  }

  function get_data_attr_wrapper($postid)
  {
    return "data-wpcr3-content=\"{$postid}\"";
  }

  function do_the_content($original_content, $fix = "wp")
  {
    global $post;

    if ($this->remote_debug()) {
      $debug = "\n<div style='display:none;'>wpcr3_info top_start</div>\n";
      $debug .= "\n<div style='display:none;'>wpcr3_info fix={$fix}</div>\n";
      $debug .= "\n<div style='display:none;'>wpcr3_info original_content={$original_content}</div>\n";
      $debug .= "\n<div style='display:none;'>wpcr3_info top_end</div>\n";
      print $debug;
    }

    if (!isset($post) || !isset($post->ID) || intval($post->ID) == 0) {
      // we need a post object to do anything useful

      if ($this->remote_debug()) {
        $debug = "\n<div style='display:none;'>wpcr3_info no post id fix={$fix}</div>\n";
        $debug .=
          "\n<div style='display:none;'>wpcr3_info above returned from filter 1</div>\n";
        print $debug;
      }

      return $original_content;
    }

    $postid = $post->ID;
    $data_attr = $this->get_data_attr_wrapper($postid);
    $already_ran = strpos($original_content, $data_attr) !== false ? 1 : 0;

    if ($this->remote_debug()) {
      $debug = "\n <div style='display:none;'>";
      $debug .=
        "\n wpcr3_info postid={$postid} fix={$fix} already_ran={$already_ran} strlen=" .
        strlen($original_content);
      $debug .= "\n </div>";
      print $debug;
    }

    // return original content if reviews should not display for this post
    $is_active_page = $this->is_active_page();

    if ($this->remote_debug()) {
      $debug = "\n <div style='display:none;'>wpcr3_info post={$post->ID} fix={$fix} already_ran={$already_ran} is_active_page={$is_active_page}</div> \n";
      $debug .=
        "\n <div style='display:none;'>wpcr3_info above returned from filter 2</div> \n";
      print $debug;
    }

    if ($already_ran === 1 || $is_active_page === 0) {
      return $original_content;
    }

    if ($this->remote_debug()) {
      $debug = "\n <div style='display:none;'>wpcr3_info on_active_page post={$post->ID} fix={$fix} is_active_page={$is_active_page}</div> \n";
      $debug .=
        "\n <div style='display:none;'>wpcr3_info above returned from filter 3</div> \n";
      print $debug;
    }

    $this->reset_active_page();

    $hideform = get_post_meta($post->ID, $this->prefix . "_hideform", true);
    $showform = $hideform !== "1";

    $opts = new stdClass();
    $opts->showform = $showform;
    $opts->showsupport = $this->options["support_us"];
    $opts->postid = $postid;
    $opts->perpage = $this->options["reviews_per_page"];
    $opts->paginate = 1;
    $opts->classes = $this->prefix . "_in_content";
    $opts->wrapper = 1;

    $reviews_content = $this->output_reviews_show($opts);
    $original_content .= $reviews_content;
    return $original_content;
  }

  function get_rating_template($rating, $enable_hover)
  {
    $data = [
      "hoverable" => $enable_hover,
      "stars" => $rating,
      "rating_width" => 20 * $rating, // 20% for each star if having 5 stars
    ];
    return wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_rating_stars"],
      $data,
    );
  }

  function get_form_field($name, $fieldArr)
  {
    $posted_name = $this->prefix . "_" . $name;

    $params = [$posted_name];
    $this->param($params);

    $required = $fieldArr["require"] == 1;

    $data = [
      "name" => $this->prefix . "_" . $name,
      "label" => wp_kses($fieldArr["label"], $this->allowedFieldTags),
      "required" => $required ? "*" : "",
      "class" => $required ? $this->prefix . "_required" : "",
      "value" => wp_kses($this->p->$posted_name, $this->allowedFieldTags),
    ];
    $field = wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_form_text_field"],
      $data,
    );
    return $field;
  }

  function get_rating_field()
  {
    $data = [
      "rating_stars" => $this->get_rating_template(0, true),
    ];
    $field = wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_form_rating_field"],
      $data,
    );
    return $field;
  }

  function get_review_field()
  {
    $posted_name = $this->prefix . "_ftext";

    $params = [$posted_name];
    $this->param($params);

    $data = [
      "value" => wp_kses($this->p->$posted_name, $this->allowedContentTags),
    ];
    $field = wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_form_review_field"],
      $data,
    );
    return $field;
  }

  // currently, because of how JS "wpcr3" object works, we can only display one form on a single page. To fix, we would need to make "wpcr3" a "new wpcr3();"
  function show_reviews_form($postid, $found_posts, $showform)
  {
    $input_fields = "";

    foreach ($this->options["standard_fields"] as $name => $fieldArr) {
      if ($fieldArr["ask"] == 1) {
        $input_fields .= $this->get_form_field($name, $fieldArr);
      }
    }

    foreach ($this->options["custom_fields"] as $name => $fieldArr) {
      if ($fieldArr["ask"] == 1) {
        $input_fields .= $this->get_form_field($name, $fieldArr);
      }
    }

    $has_required_fields =
      strpos($input_fields, $this->prefix . "_required") !== false;
    $rating_field = $this->get_rating_field();
    $review_field = $this->get_review_field();

    $data = [
      "input_fields" => $input_fields,
      "rating_field" => $rating_field,
      "review_field" => $review_field,
      "has_required_fields" => $has_required_fields,
      "postid" => $postid,
      "found_posts" => $found_posts,
      "showform" => $showform,
    ];

    return wpcr_Goatee::fill(
      $this->options["templates"]["frontend_review_form"],
      $data,
    );
  }

  function generateTitle($fname)
  {
    $fname = strlen($fname) === 0 ? "Anonymous" : $fname;
    $datetime = date("m/d/Y h:i");
    return "{$fname} @ {$datetime}";
  }

  function isXssAttempt($s)
  {
    // lowercase, url decode, decode entities, remove spaces, tabs, linebreaks, chars 0-31,127
    $s = preg_replace(
      '/[\s\x00-\x1F\x7F]/',
      "",
      html_entity_decode(urldecode(strtolower($s))),
    );

    if (stripos($s, "javascript:") !== false) {
      return true;
    }
    if (stripos($s, "eval(") !== false) {
      return true;
    }
    if (stripos($s, "atob(") !== false) {
      return true;
    }
    if (stripos($s, "alert(") !== false) {
      return true;
    }
    if (stripos($s, "settimeout(") !== false) {
      return true;
    }
    if (stripos($s, "setinterval(") !== false) {
      return true;
    }
    if (stripos($s, "<") !== false) {
      return true;
    }
    if (stripos($s, ">") !== false) {
      return true;
    }
    if (stripos($s, "onerror") !== false) {
      return true;
    }
    if (stripos($s, "onload") !== false) {
      return true;
    }
    if (stripos($s, "onfocus") !== false) {
      return true;
    }
    if (stripos($s, "onblur") !== false) {
      return true;
    }
    if (stripos($s, "onchange") !== false) {
      return true;
    }
    if (stripos($s, "oninput") !== false) {
      return true;
    }
    if (stripos($s, "onclick") !== false) {
      return true;
    }
    if (stripos($s, "onpropertychange") !== false) {
      return true;
    }
    if (stripos($s, "onreadystatechange") !== false) {
      return true;
    }
    if (stripos($s, "onmouse") !== false) {
      return true;
    } // onmouse*
    if (stripos($s, "onkey") !== false) {
      return true;
    } // onkey*
    if (stripos($s, '\"') !== false) {
      return true;
    }
    if (stripos($s, "\'") !== false) {
      return true;
    }

    return false;
  }

  function ajax()
  {
    header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Pragma: no-cache");
    header("Content-type: application/json");

    $rtn = new stdClass();
    $rtn->err = [];
    $rtn->success = false;

    $posted = new stdClass();
    foreach ($this->p as $k => $v) {
      $k = str_replace($this->prefix . "_", "", $k);
      $posted->$k = $v;
    }

    $params = [
      "ajaxAct2",
      "postid",
      "checkid2",
      "fconfirm1",
      "fconfirm2",
      "fconfirm3",
      "url",
      "website",
      "femail",
      "fname",
      "frating",
      "ftitle",
      "fwebsite",
      "pageOpts",
      "page",
      "on_postid",
    ];

    foreach ($this->options["custom_fields"] as $name => $fieldArr) {
      $params[] = $name;
    }

    $this->param($params, $posted);

    // loop over expected params to check XSS
    // because plugins/servers sometimes inject vars into $_GET and $_POST, and...
    // $this->p, $this->posted then ends up with more than expected
    foreach ($params as $k) {
      $v = $posted->$k;

      if ($this->isXssAttempt($v) === true) {
        $rtn->err[] = "You have failed the spambot check. Code 0";
        die(json_encode($rtn));
      }

      $posted->$k = trim(strip_tags($v));
    }

    $ajaxAct = $posted->ajaxAct2;
    $postid = (int) $posted->postid;
    $checkId = (int) $posted->checkid2;
    $checkIdExpect = $postid * 42 - $postid;

    if ($ajaxAct === "form") {
      if ($checkId != $checkIdExpect) {
        $rtn->err[] = "You have failed the spambot check. Code 1";
      }
      if ($posted->fconfirm1 != "0") {
        $rtn->err[] = "You have failed the spambot check. Code 2";
      }
      if ($posted->fconfirm2 != "1") {
        $rtn->err[] = "You have failed the spambot check. Code 3";
      }
      if ($posted->fconfirm3 != "1") {
        $rtn->err[] = "You have failed the spambot check. Code 4";
      }
      if ($posted->url != "") {
        $rtn->err[] = "You have failed the spambot check. Code 5";
      }
      if ($posted->website != "") {
        $rtn->err[] = "You have failed the spambot check. Code 6";
      }
      if (
        $posted->femail != "" &&
        filter_var($posted->femail, FILTER_VALIDATE_EMAIL) == false
      ) {
        $rtn->err[] = "Please enter a valid email address.";
      }

      if (count($rtn->err)) {
        die(json_encode($rtn));
      } // die here if we failed any spambot checks

      // passed all spambot checks, continue

      $title = $this->generateTitle($posted->fname);

      $newpost = [
        "post_author" => 1,
        "post_date" => date("Y-m-d H:i:s"),
        "post_content" => nl2br($posted->ftext),
        "post_status" => "pending",
        "post_title" => $title,
        "post_type" => $this->prefix . "_review",
      ];
      $newpostid = wp_insert_post($newpost, true);

      update_post_meta(
        $newpostid,
        $this->prefix . "_review_ip",
        $_SERVER["REMOTE_ADDR"],
      );

      if (isset($posted->postid)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_post",
          $posted->postid,
        );
      }
      if (isset($posted->fname)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_name",
          $posted->fname,
        );
      }
      if (isset($posted->femail)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_email",
          $posted->femail,
        );
      }
      if (isset($posted->frating)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_rating",
          $posted->frating,
        );
      }
      if (isset($posted->ftitle)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_title",
          $posted->ftitle,
        );
      }
      if (isset($posted->fwebsite)) {
        update_post_meta(
          $newpostid,
          $this->prefix . "_review_website",
          $posted->fwebsite,
        );
      }

      foreach ($this->options["custom_fields"] as $name => $fieldArr) {
        if ($fieldArr["ask"] == 1 && isset($posted->$name)) {
          update_post_meta(
            $newpostid,
            $this->prefix . "_" . $name,
            $posted->$name,
          );
        }
      }

      $datetime = date("m/d/Y h:i");
      @wp_mail(
        get_bloginfo("admin_email"),
        "WP Customer Reviews: New Review Posted on {$datetime}",
        "A new review has been posted on " .
          get_bloginfo("name") .
          " via WP Customer Reviews. \n\nYou will need to approve this review before it will appear on your site.",
      );
    } elseif ($ajaxAct === "pager") {
      $opts = json_decode($posted->pageOpts);
      $opts->thispage = $posted->page;
      $opts->ajax = 1;
      $opts->showsupport = 0;
      $opts->on_postid = $posted->on_postid;
      $rtn->output = $this->output_reviews_show($opts);
    } else {
      $rtn->err[] = "You have failed the spambot check. Code 7";
      die(json_encode($rtn));
    }

    $rtn->success = true;
    die(json_encode($rtn));
  }

  function getAjaxURL()
  {
    return admin_url("admin-ajax.php") . "?action=wpcr3-ajax";
    //return $this->getpluginurl()."include/ajax.php";
  }

  // used in extended classes to grab already-set information we care about from main class
  function setSharedVars($parentClass)
  {
    $this->plugin_info = &$parentClass->plugin_info; // array by &reference
    $this->plugin_version = $this->plugin_info["Version"];
    $this->pro = $parentClass->pro;

    $this->options = &$parentClass->options; // array by &reference
    $this->p = $parentClass->p; // object is already by &reference
  }

  function init()
  {
    $this->include_pro();

    add_action("admin_menu", [&$this, "admin_menu"]); // adding menu items to admin must be done in admin_menu which gets executed BEFORE admin_init
    add_action("admin_init", [&$this, "admin_init"]);
    add_filter("the_content", [&$this, "do_the_content"], 15); // prio 15 makes sure this hits after wptexturize and other garbage that likes to destroy our output

    // "Wordpress SEO - Yoast" hijacks the_content and breaks all kinds of plugins
    // luckily they provide a filter to fix it
    // add_filter('wpseo_pre_analysis_post_content', array(&$this, 'do_the_content_wpseo'), 15);

    // $this->print_filters_for('the_content'); exit();

    add_action("wp_ajax_" . $this->prefix . "-ajax", [&$this, "ajax"]);
    add_action("wp_ajax_nopriv_" . $this->prefix . "-ajax", [&$this, "ajax"]);

    $this->plugin_info = $this->plugin_get_info();
    $this->plugin_version = $this->plugin_info["Version"];

    $this->make_p_obj(); // make P variables object
    $this->get_options(); // populate the options array
    $this->create_post_type();

    // remove any existing shortcode to allow v2 and v3 to coexist
    remove_shortcode("WPCR_INSERT");
    remove_shortcode("WPCR_SHOW");

    add_shortcode("WPCR_INSERT", [&$this, "shortcode_insert"]);
    add_shortcode("WPCR_SHOW", [&$this, "shortcode_show"]);
    add_shortcode("WPCR_HCARD", [&$this, "shortcode_hcard"]); // deprecated, returns blank

    // we insert styles/scripts in init because some themes are horrible
    wp_register_style(
      "wp-customer-reviews-3-frontend",
      $this->getpluginurl() . "css/wp-customer-reviews.css",
      [],
      $this->plugin_version,
    );
    wp_register_script(
      "wp-customer-reviews-3-frontend",
      $this->getpluginurl() . "js/wp-customer-reviews.js",
      ["jquery"],
      $this->plugin_version,
    );
    wp_enqueue_style("wp-customer-reviews-3-frontend");
    wp_enqueue_script("wp-customer-reviews-3-frontend");
  }

  function create_post_type()
  {
    $defaults1 = [
      "labels" => [],
      "public" => false,
      "exclude_from_search" => true,
      "publicly_queryable" => false,
      "show_in_nav_menus" => false,
      "show_ui" => true,
      "show_in_menu" => $this->prefix . "_view_reviews",
      "menu_position" => 25,
      "show_in_admin_bar" => false,
      "has_archive" => false,
      "rewrite" => false,
      "supports" => ["title"],
      "map_meta_cap" => true,
    ];

    $defaults2 = $defaults1;
    $defaults2["labels"] = [
      "name" => "WP Customer Reviews",
      "singular_name" => "Review",
      "menu_name" => "All Reviews",
      "add_new_item" => "Add New Customer Review",
      "edit_item" => "Edit Customer Review",
      "new_item" => "New Customer Review",
      "view_item" => "View Customer Review",
      "search_items" => "Search Customer Reviews",
      "not_found" => "No Reviews Found",
      "not_found_in_trash" => "No Reviews Found in Trash",
    ];
    $defaults2["supports"] = ["title", "editor"];
    $err = register_post_type($this->prefix . "_review", $defaults2);
  }

  function shortcode_insert()
  {
    $this->force_active_page = "shortcode_insert";
    return $this->do_the_content("", "shortcode_insert");
  }

  function strip_trim($val)
  {
    return trim(strip_tags($val));
  }

  function strip_trim_intval($val)
  {
    return intval($this->strip_trim($val));
  }

  function shortcode_show($atts)
  {
    $attArr = shortcode_atts(
      [
        "postid" => "all",
        "num" => 5,
        "paginate" => 1,
        "perpage" => 5,
        "hidecustom" => 0,
        "snippet" => 0,
        "more" => "",
        "showform" => 1,
        "hidereviews" => 0,
        "hideresponse" => 0,
      ],
      $atts,
    );

    $opts = new stdClass();
    foreach ($attArr as $key => $val) {
      $opts->$key = $val;
    }

    $opts->postid = $this->strip_trim($opts->postid);

    if (strtolower($opts->postid) == "all") {
      $opts->postid = -1;
    } // -1 queries all reviews
    $opts->morelink = $opts->more;

    $intArr = [
      "postid",
      "num",
      "paginate",
      "perpage",
      "hidecustom",
      "snippet",
      "showform",
      "hidereviews",
      "hideresponse",
    ];
    foreach ($intArr as $key) {
      $opts->$key = isset($opts->$key)
        ? $this->strip_trim_intval($opts->$key)
        : 0;
    }

    if ($opts->postid === -1) {
      $opts->showform = 0;
    } // do not show form if postid is "all"
    $opts->showsupport = 0;
    if ($opts->showform == 1) {
      $opts->showsupport = 1;
    }

    $opts->wrapper = 1;

    return $this->output_reviews_show($opts);
  }

  // deprecated, returns blank
  function shortcode_hcard($atts)
  {
    return "";
  }

  function activate()
  {
    add_option($this->prefix . "_gotosettings", true); // used for redirecting to settings page upon initial activation
  }

  function deactivate()
  {
    // do not fire on upgrading plugin or upgrading WP - only on true manual deactivation
    if (isset($this->p->action) && $this->p->action == "deactivate") {
      $this->admin_function("notify_activate", 2);
    }
  }

  function template($name)
  {
    return $this->options["templates"][$name];
  }

  function getpluginurl()
  {
    return trailingslashit(plugins_url(basename(dirname(__FILE__))));
  }

  function getpluginurl_abs()
  {
    $url = $this->getpluginurl();
    if (strpos($url, "://") === false) {
      $proto =
        !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] != "off"
          ? "https://"
          : "http://";
      $url = $proto . $_SERVER["HTTP_HOST"] . $url;
    }
    return $url;
  }

  function getplugindir()
  {
    return trailingslashit(
      WP_PLUGIN_DIR .
        "/" .
        str_replace(basename(__FILE__), "", plugin_basename(__FILE__)),
    );
  }
}

function start_wpcr3()
{
  $WPCustomerReviews3 = new WPCustomerReviews3();
  $WPCustomerReviews3->start();
  return $WPCustomerReviews3;
}

$WPCustomerReviews3 = start_wpcr3();