Files
dw-sheet-data-checker/includes/class-Shortcode.php

840 lines
29 KiB
PHP

<?php
class CHECKER_SHORTCODE extends SHEET_DATA_CHECKER_PRO
{
/**
* A reference to an instance of this class.
*/
private static $instance;
/**
* Returns an instance of this class.
*/
public static function get_instance()
{
return self::$instance;
}
/**
* Initializes the plugin by setting filters and administration functions.
*/
public function __construct()
{
// Load security class
if (!class_exists("CHECKER_SECURITY")) {
require_once SHEET_CHECKER_PRO_PATH . "includes/class-Security.php";
}
// Ensure CAPTCHA helper is available
if (!class_exists("CHECKER_CAPTCHA_HELPER")) {
require_once SHEET_CHECKER_PRO_PATH . "includes/helpers/class-Captcha-Helper.php";
}
add_shortcode("checker", [$this, "content"]);
add_action("wp_enqueue_scripts", [$this, "enqueue"]);
add_action("wp_ajax_checker_public_validation", [
$this,
"checker_public_validation",
]);
add_action("wp_ajax_nopriv_checker_public_validation", [
$this,
"checker_public_validation",
]);
add_action("wp_ajax_checker_load_all_data", [
$this,
"checker_load_all_data",
]);
add_action("wp_ajax_nopriv_checker_load_all_data", [
$this,
"checker_load_all_data",
]);
add_action("wp_ajax_checker_clear_cache", [
$this,
"checker_clear_cache",
]);
add_action("wp_ajax_nopriv_checker_clear_cache", [
$this,
"checker_clear_cache_public",
]);
}
public function enqueue()
{
wp_enqueue_style(
"datatable",
"https://cdn.datatables.net/1.13.7/css/jquery.dataTables.min.css",
[],
"all",
);
wp_enqueue_style(
"checker-pro",
SHEET_CHECKER_PRO_URL .
"assets/public.css?ver=" .
SHEET_CHECKER_PRO_VERSION,
[],
"all",
);
wp_enqueue_script(
"datatable",
"https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js",
["jquery"],
null,
true
);
wp_enqueue_script(
"checker-pro",
SHEET_CHECKER_PRO_URL . "assets/public.js",
["jquery", "datatable"],
SHEET_CHECKER_PRO_VERSION,
true
);
// Pass nonce and i18n strings to JavaScript
wp_localize_script("checker-pro", "checkerSecurity", [
"nonce" => wp_create_nonce("checker_ajax_nonce"),
"ajaxurl" => admin_url("admin-ajax.php"),
"i18n" => [
"refresh_page" => __("Refresh Page", "sheet-data-checker-pro"),
"session_expired" => __("Session expired. Please refresh the page and try again.", "sheet-data-checker-pro"),
"recaptcha_failed" => __("reCAPTCHA verification failed. Please try again.", "sheet-data-checker-pro"),
"turnstile_failed" => __("Turnstile verification failed. Please try again.", "sheet-data-checker-pro"),
"rate_limited" => __("Too many attempts. Please try again later.", "sheet-data-checker-pro"),
"security_error" => __("Security validation failed.", "sheet-data-checker-pro"),
"loading" => __("Loading...", "sheet-data-checker-pro"),
"searching" => __("Searching...", "sheet-data-checker-pro"),
"error_occurred" => __("An error occurred. Please try again.", "sheet-data-checker-pro"),
],
]);
}
public function content($atts, $content = null)
{
if (!isset($atts["id"])) {
return;
}
$post_id = $atts["id"];
$checker = get_post_meta($post_id, "checker", true);
// Load CAPTCHA scripts if enabled
if (class_exists('CHECKER_CAPTCHA_HELPER')) {
CHECKER_CAPTCHA_HELPER::load_captcha_scripts($post_id);
}
$checker = wp_parse_args($checker, [
"link" => "",
"description" => "",
"card" => [
"width" => 500,
"background" => "#cccccc",
"bg_opacity" => 50,
"border_radius" => 1,
"box_shadow" => "10px 5px 15px -5px",
"box_shadow_color" => "#333333",
"title" => "#333333",
"title_align" => "left",
"description" => "#333333",
"description_align" => "left",
"divider" => "#333333",
"divider_width" => 1,
],
"field" => [
"label" => "block",
"label-color" => "#333333",
],
"fields" => [],
"search_button" => [
"text" => "Search",
"bg_color" => "#cccccc",
"text_color" => "#333333",
"position" => "flex-end",
],
"back_button" => [
"text" => "Back",
"bg_color" => "#cccccc",
"text_color" => "#333333",
"position" => "flex-start",
],
"result" => [
"display" => "vertical-tabel",
"header" => "#333333",
"value" => "#333333",
"columns" => [],
"border_width" => 1,
],
]);
$url = isset($checker["link"]) ? (string)$checker["link"] : '';
$link_format = $url ? substr($url, -3) : 'csv';
// Set the delimiter based on the format
$delimiter = $link_format == "tsv" ? "\t" : ","; // Use tab for TSV, comma for CSV
// Validate allowed host
if (!$this->is_allowed_sheet_url($url)) {
wp_send_json_error([
"message" => __("Sheet URL is not allowed. Please use an approved domain.", "sheet-data-checker-pro"),
"type" => "error",
]);
return;
}
// Use WordPress HTTP API instead of fopen for better server compatibility
$data = $this->fetch_remote_csv_data($url, $delimiter);
$background_color = isset($checker["card"]["background"]) ? $checker["card"]["background"] : '#ffffff';
if ($checker["card"]["bg_opacity"] < 100) {
$background_color =
$checker["card"]["background"] .
"" .
$checker["card"]["bg_opacity"];
}
$render = "";
$render .=
'<div class="dw-checker-container" id="checker-' . $post_id . '" data-hp-name="">';
$render .=
'<form class="dw-checker-wrapper dw-checker-form"
style="max-width: 100%;
background-color: ' .
$background_color .
';
width: ' .
$checker["card"]["width"] .
'px;
padding: ' .
$checker["card"]["padding"] .
'em;
border-radius: ' .
$checker["card"]["border_radius"] .
'em;
box-shadow: ' .
$checker["card"]["box_shadow"] .
" " .
$checker["card"]["box_shadow_color"] .
';
">';
$render .=
'<div class="dw-checker-title"
style="color: ' .
$checker["card"]["title"] .
';
text-align: ' .
$checker["card"]["title_align"] .
';"
>' .
get_the_title($post_id) .
"</div>";
$render .=
'<div class="dw-checker-description"
style="color: ' .
$checker["card"]["description"] .
';
text-align: ' .
$checker["card"]["description_align"] .
';"
>' .
$checker["description"] .
"</div>";
$render .=
'<hr class="dw-checker-divider"
style="border-color: ' .
$checker["card"]["divider"] .
';
border-width: ' .
$checker["card"]["divider_width"] .
'px;">';
$render .= '<div class="dw-checker-form-fields">';
if (isset($checker["fields"]) && !empty($checker["fields"])) {
foreach ($checker["fields"] as $key => $field) {
if ($field["type"] == "text") {
$render .=
'<div class="dw-checker-field">
<label for="' .
$key .
'" style="color: ' .
$checker["field"]["label-color"] .
";display: " .
$checker["field"]["label"] .
';">
' .
$field["label"] .
'
</label>
<input name="' .
$key .
'" placeholder="' .
$field["placeholder"] .
'" class="dw-checker-inputs" data-kolom="' .
$field["kolom"] .
'" required/>
</div>';
} else {
$options = "";
$option_array = [];
foreach ($data as $all_data) {
foreach ($all_data as $_key => $_value) {
if (
$_key == $field["kolom"] &&
!in_array($_value, $option_array)
) {
$option_array[] = $_value;
}
}
}
asort($option_array);
if (!empty($option_array)) {
foreach ($option_array as $val) {
$options .=
'<option value="' .
$val .
'">' .
$val .
"</option>";
}
}
$render .=
'<div class="dw-checker-field">
<label for="' .
$key .
'" style="color: ' .
$checker["field"]["label-color"] .
";display: " .
$checker["field"]["label"] .
';">
' .
$field["kolom"] .
'
</label>
<select name="' .
$key .
'" placeholder="' .
$field["placeholder"] .
'" class="dw-checker-inputs" data-kolom="' .
$field["kolom"] .
'" required>
<option value="" disabled selected>-- ' .
$field["placeholder"] .
' --</option>
' .
$options .
'
</select>
</div>';
}
}
}
$render .= "</div>";
// CAPTCHA fields
if (class_exists('CHECKER_CAPTCHA_HELPER')) {
$render .= CHECKER_CAPTCHA_HELPER::get_captcha_fields($post_id);
}
$render .=
'<hr class="dw-checker-divider"
style="border-color: ' .
$checker["card"]["divider"] .
';
border-width: ' .
$checker["card"]["divider_width"] .
'px;">';
// Add honeypot field if enabled (invisible to users, catches bots)
if (CHECKER_SECURITY::is_enabled($checker, 'honeypot')) {
$hp_name = 'hp_' . wp_generate_password(8, false, false);
$render = str_replace('data-hp-name=""', 'data-hp-name="' . esc_attr($hp_name) . '"', $render);
$render .= '<div class="dw-checker-hp-field" style="position:absolute;left:-9999px;top:-9999px;opacity:0;height:0;width:0;overflow:hidden;" aria-hidden="true">';
$render .= '<label for="' . esc_attr($hp_name . '_' . $post_id) . '">' . esc_html__('Leave this field empty', 'sheet-data-checker-pro') . '</label>';
$render .= '<input type="text" name="' . esc_attr($hp_name) . '" id="' . esc_attr($hp_name . '_' . $post_id) . '" value="" tabindex="-1" autocomplete="off" data-hp-field="1">';
$render .= '</div>';
}
$render .=
'<div class="dw-checker-buttons dw-checker-form-button" style="justify-content: ' .
$checker["search_button"]["position"] .
'">';
$render .=
'<button type="button" data-checker="' .
$post_id .
'" class="search-button"
data-btn-text="' .
$checker["search_button"]["text"] .
'"
style="background-color: ' .
$checker["search_button"]["bg_color"] .
';
color: ' .
$checker["search_button"]["text_color"] .
';">
' .
$checker["search_button"]["text"] .
'
</button>
</div>';
$render .= "</form>";
$render .=
'<div class="dw-checker-wrapper dw-checker-result"
style="display:none; max-width: 100%;
background-color: ' .
$checker["card"]["background"] .
';
width: ' .
$checker["card"]["width"] .
'px;
padding: ' .
$checker["card"]["padding"] .
'em;
border-radius: ' .
$checker["card"]["border_radius"] .
'em;
box-shadow: ' .
$checker["card"]["box_shadow"] .
" " .
$checker["card"]["box_shadow_color"] .
';
">';
$render .=
'<div class="dw-checker-title"
style="color: ' .
$checker["card"]["title"] .
';
text-align: ' .
$checker["card"]["title_align"] .
';"
></div>';
$render .=
'<div class="dw-checker-description"
style="color: ' .
$checker["card"]["description"] .
';
text-align: ' .
$checker["card"]["description_align"] .
';"
></div>';
$render .=
'<hr class="dw-checker-divider"
style="border-color: ' .
$checker["card"]["divider"] .
';
border-width: ' .
$checker["card"]["divider_width"] .
'px;">';
$render .= '<div class="dw-checker-results"></div>';
$render .=
'<hr class="dw-checker-divider"
style="border-color: ' .
$checker["card"]["divider"] .
';
border-width: ' .
$checker["card"]["divider_width"] .
'px;">';
$render .=
'<div class="dw-checker-buttons dw-checker-result-button" style="justify-content: ' .
$checker["back_button"]["position"] .
'">';
$render .=
'<button type="button" class="back-button" data-checker=' .
$post_id .
'
style="background-color: ' .
$checker["back_button"]["bg_color"] .
';
color: ' .
$checker["back_button"]["text_color"] .
';">
' .
$checker["back_button"]["text"] .
'
</button>';
$render .= "</div>";
$render .= "</div>";
$render .= "</div>";
$render .= '<div class="dw-checker-bottom-results"></div>';
// Pass settings to frontend as data attributes
$render .=
'<script type="application/json" id="checker-settings-' .
$post_id .
'" class="checker-settings-data">';
$render .= json_encode([
"checker_id" => $post_id,
"initial_display" =>
$checker["result"]["initial_display"] ?? "hidden",
"filter_mode" => $checker["result"]["filter_mode"] ?? "search",
"max_records" => $checker["result"]["max_records"] ?? 100,
"url_params_enabled" => $checker["url_params"]["enabled"] ?? "no",
"url_params_auto_search" =>
$checker["url_params"]["auto_search"] ?? "no",
]);
$render .= "</script>";
return $render;
}
/**
* Fetch remote CSV/TSV data using WordPress HTTP API
* Replaces fopen() for better server compatibility
*/
private function fetch_remote_csv_data($url, $delimiter, $limit = null, $force_refresh = false)
{
$data = [];
// Build cache key
$cache_key = 'checker_csv_' . md5($url . $delimiter . $limit);
// Check cache first (unless force refresh)
if (!$force_refresh && !is_admin()) {
$cached_data = get_transient($cache_key);
if ($cached_data !== false) {
return $cached_data;
}
}
// Use WordPress HTTP API to fetch remote file
$response = wp_remote_get($url, [
'timeout' => 30,
'sslverify' => false // For local development
]);
if (is_wp_error($response)) {
error_log(
"Failed to fetch remote file: " .
$response->get_error_message(),
);
return $data;
}
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
error_log("Empty response from remote file: " . $url);
return $data;
}
// Parse CSV/TSV data
$lines = explode("\n", $body);
if (empty($lines)) {
return $data;
}
// Get headers from first line
$keys = str_getcsv($lines[0], $delimiter);
// Process data rows
$count = 0;
for ($i = 1; $i < count($lines); $i++) {
if (empty(trim($lines[$i]))) {
continue; // Skip empty lines
}
$row = str_getcsv($lines[$i], $delimiter);
if (count($keys) === count($row)) {
$data[] = array_combine($keys, $row);
$count++;
// Apply limit if specified
if ($limit && $count >= $limit) {
break;
}
}
}
// Cache the data for 5 minutes (unless in admin)
if (!is_admin()) {
set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
}
return $data;
}
/**
* AJAX handler to clear cache for a specific checker (admin only)
*/
public function checker_clear_cache()
{
check_ajax_referer('checker_ajax_nonce', 'security');
if (!current_user_can('edit_posts')) {
wp_send_json_error(['message' => 'Unauthorized']);
return;
}
$post_id = isset($_REQUEST['checker_id']) ? intval($_REQUEST['checker_id']) : 0;
if (!$post_id) {
wp_send_json_error(['message' => 'Invalid checker ID']);
return;
}
$checker = get_post_meta($post_id, 'checker', true);
if (!$checker || !isset($checker['link'])) {
wp_send_json_error(['message' => 'Checker not found']);
return;
}
$url = (string)$checker['link'];
$link_format = $url ? substr($url, -3) : 'csv';
$delimiter = $link_format == 'tsv' ? "\t" : ",";
// Clear all possible cache variations for this checker
$limits = [null, 10, 100, 500, 1000];
$cleared = 0;
foreach ($limits as $limit) {
$cache_key = 'checker_csv_' . md5($url . $delimiter . $limit);
if (delete_transient($cache_key)) {
$cleared++;
}
}
wp_send_json_success([
'message' => sprintf(__('Cache cleared successfully (%d entries)', 'sheet-data-checker-pro'), $cleared),
'cleared' => $cleared
]);
}
/**
* AJAX handler to clear cache for frontend users (public)
*/
public function checker_clear_cache_public()
{
check_ajax_referer('checker_ajax_nonce', 'security');
$post_id = isset($_REQUEST['checker_id']) ? intval($_REQUEST['checker_id']) : 0;
if (!$post_id) {
wp_send_json_error(['message' => __('Invalid checker ID', 'sheet-data-checker-pro')]);
return;
}
$checker = get_post_meta($post_id, 'checker', true);
if (!$checker || !isset($checker['link'])) {
wp_send_json_error(['message' => __('Checker not found', 'sheet-data-checker-pro')]);
return;
}
$url = (string)$checker['link'];
$link_format = $url ? substr($url, -3) : 'csv';
$delimiter = $link_format == 'tsv' ? "\t" : ",";
// Clear all possible cache variations for this checker
$limits = [null, 10, 100, 500, 1000];
$cleared = 0;
foreach ($limits as $limit) {
$cache_key = 'checker_csv_' . md5($url . $delimiter . $limit);
if (delete_transient($cache_key)) {
$cleared++;
}
}
wp_send_json_success([
'message' => __('Data refreshed successfully!', 'sheet-data-checker-pro'),
'cleared' => $cleared
]);
}
public function checker_public_validation()
{
$post_id = isset($_REQUEST["checker_id"]) ? intval($_REQUEST["checker_id"]) : 0;
if (!$post_id) {
wp_send_json_error(["message" => "Invalid checker ID", "type" => "error"]);
return;
}
$checker = get_post_meta($post_id, "checker", true);
// Enforce nonce
if (!isset($_REQUEST['security']) || !check_ajax_referer('checker_ajax_nonce', 'security', false)) {
wp_send_json_error([
"message" => __("Security check failed. Please refresh the page.", "sheet-data-checker-pro"),
"type" => "nonce_expired",
]);
return;
}
// Unified security verification (rate limit, honeypot, reCAPTCHA, Turnstile)
$security_error = CHECKER_SECURITY::verify_all_security($post_id, $checker, $_REQUEST);
if ($security_error !== null) {
wp_send_json_error([
"message" => $security_error["message"],
"type" => $security_error["type"],
]);
return;
}
$url = isset($checker["link"]) ? (string)$checker["link"] : '';
$link_format = $url ? substr($url, -3) : 'csv';
// Set the delimiter based on the format
$delimiter = $link_format == "tsv" ? "\t" : ","; // Use tab for TSV, comma for CSV
// Use WordPress HTTP API instead of fopen for better server compatibility
$data = $this->fetch_remote_csv_data($url, $delimiter);
$validator = isset($_REQUEST["validate"]) && is_array($_REQUEST["validate"]) ? $_REQUEST["validate"] : [];
$validation = [];
foreach ($validator as $validate) {
$kolom = isset($validate["kolom"]) ? (string)$validate["kolom"] : '';
$val = isset($validate["value"]) ? (string)$validate["value"] : '';
if ($kolom !== '') {
$validation[$kolom] = $val;
}
}
$validator_count = count($validator);
$result = [];
if (!empty($data)) {
foreach ($data as $row) {
$valid = [];
foreach ($row as $header => $value) {
// Ensure string types to avoid null deprecation warnings
$header = ($header !== null) ? (string)$header : '';
$value = ($value !== null) ? (string)$value : '';
$id = "_" . strtolower(str_replace(" ", "_", $header));
$include = false;
if (isset($validation[$header])) {
$validation_value = isset($validation[$header]) ? (string)$validation[$header] : '';
if (
isset($checker["fields"][$id]["match"]) &&
$checker["fields"][$id]["match"] == "match" &&
strtolower($value) == strtolower($validation_value)
) {
$include = true;
}
if (
isset($checker["fields"][$id]["match"]) &&
$checker["fields"][$id]["match"] == "contain" &&
$validation_value !== '' &&
false !== strpos(strtolower($value), strtolower($validation_value))
) {
$include = true;
}
if ($include) {
$valid[$header] = $value;
}
}
}
if ($validator_count !== count($valid)) {
continue;
}
$result[] = $row;
}
}
$send = [
"count" => count($result),
"rows" => $result,
"settings" => $checker["result"],
"output" => $checker["output"],
];
wp_send_json($send);
}
/**
* Load all data from sheet (for show all mode)
*/
public function checker_load_all_data()
{
$post_id = isset($_REQUEST["checker_id"])
? intval($_REQUEST["checker_id"])
: 0;
$limit = isset($_REQUEST["limit"]) ? intval($_REQUEST["limit"]) : 100;
$is_initial_load = isset($_REQUEST["initial_load"]) && $_REQUEST["initial_load"] === "yes";
if (!$post_id) {
wp_send_json_error(["message" => "Invalid checker ID", "type" => "error"]);
return;
}
$checker = get_post_meta($post_id, "checker", true);
if (!$checker || !isset($checker["link"])) {
wp_send_json_error(["message" => "Checker not found", "type" => "error"]);
return;
}
// Enforce nonce
if (!isset($_REQUEST['security']) || !check_ajax_referer('checker_ajax_nonce', 'security', false)) {
wp_send_json_error([
"message" => __("Security check failed. Please refresh the page.", "sheet-data-checker-pro"),
"type" => "nonce_expired",
]);
return;
}
// For initial load in show-all mode, skip CAPTCHA but still check rate limit and honeypot
// This allows the page to load while CAPTCHA widget renders
$skip_captcha = $is_initial_load && CHECKER_SECURITY::get_setting($checker, 'result', 'skip_captcha_initial', 'yes') === 'yes';
// Unified security verification
$security_error = CHECKER_SECURITY::verify_all_security($post_id, $checker, $_REQUEST, $skip_captcha);
if ($security_error !== null) {
wp_send_json_error([
"message" => $security_error["message"],
"type" => $security_error["type"],
]);
return;
}
$url = isset($checker["link"]) ? (string)$checker["link"] : '';
$link_format = $url ? substr($url, -3) : 'csv';
$delimiter = $link_format == "tsv" ? "\t" : ",";
if (!$this->is_allowed_sheet_url($url)) {
wp_send_json_error([
"message" => __("Sheet URL is not allowed. Please use an approved domain.", "sheet-data-checker-pro"),
"type" => "error",
]);
return;
}
// Use WordPress HTTP API instead of fopen for better server compatibility
$data = $this->fetch_remote_csv_data($url, $delimiter, $limit);
wp_send_json([
"count" => count($data),
"rows" => $data,
"settings" => $checker["result"],
"output" => $checker["output"],
"url_params" => $checker["url_params"] ?? [],
"filter_mode" => $checker["result"]["filter_mode"] ?? "search",
]);
}
/**
* Allowlist check for sheet URL host
*/
private function is_allowed_sheet_url($url)
{
$host = parse_url($url, PHP_URL_HOST);
if (!$host) {
return false;
}
$allowed = apply_filters(
'sheet_checker_allowed_hosts',
[
'docs.google.com',
'drive.google.com',
'docs.googleusercontent.com',
wp_parse_url(home_url(), PHP_URL_HOST),
]
);
$allowed = array_filter(array_unique(array_map('strtolower', $allowed)));
return in_array(strtolower($host), $allowed, true);
}
}