shell bypass 403
<?php
namespace FluentFormPro\classes;
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
use FluentForm\App\Helpers\Helper;
use FluentForm\Framework\Helpers\ArrayHelper as Arr;
use FluentForm\App\Services\FormBuilder\EditorShortCode;
use FluentForm\App\Modules\Form\FormFieldsParser;
class AdvancedEntriesSearch
{
protected $supportedColumns = [];
protected $numericColumns = [];
protected $entryDetailsTableAlies = [];
public function init()
{
add_filter('fluentform/entries_vars', [$this, 'getAdvancedFilterOptions'], 10, 2);
add_filter('fluentform/apply_entries_advance_filter', [$this, 'applyAdvancedFilter'], 10, 2);
}
public function getAdvancedFilterOptions($data, $form)
{
$submissionCodes = EditorShortCode::getSubmissionShortcodes();
$fields = FormFieldsParser::getInputsByElementTypes($form, $this->supportedFields(), ['label', 'element', 'options', 'attributes', 'settings']);
$fieldCodes = [];
foreach ($fields as $name => $field) {
// Remove repeater subfield
if ('.*' === substr($name, -strlen('.*'))) {
continue;
}
$arr = [
'label' => $field['label'] ? $field['label'] : $name,
'value' => $name,
];
$element = Arr::get($field, 'element');
if (in_array($element, ['input_number', 'item_quantity_component'])) {
$arr['type'] = 'numeric';
} elseif (in_array($element, ['input_radio', 'select', 'input_checkbox', 'multi_payment_component'])) {
$arr['type'] = 'selections';
$arr['options'] = Arr::get($field, 'options');
$arr['is_multiple'] = false;
if ('input_checkbox' == $element || Arr::isTrue($field, 'attributes.multiple')) {
$arr['is_multiple'] = true;
}
} elseif ('input_date' == $element) {
$arr['type'] = 'dates';
$format = Arr::get($field, 'settings.date_format');
$arr['format'] = $format;
$dateType = Arr::get($this->getDatesInfo(), $format . '.type');
if ('time' == $dateType) {
$arr['type'] = 'time';
}
$arr['date_type'] = $dateType ?: 'date';
} else {
$arr['type'] = 'text';
}
$fieldCodes[] = $arr;
}
$formShortCodes = [
'inputs' => [
'label' => __('Inputs', 'fluentformpro'),
'value' => 'inputs',
'children' => array_filter($fieldCodes)
]
];
$userCodes = $this->formatAdvancedFilters([
'title' => __('User', 'fluentformpro'),
'value' => 'user',
'shortcodes' => $this->getUserCodes()
]);
$submissionCodes['value'] = 'entry-attribute';
$submissionCodes['shortcodes'] = array_filter($submissionCodes['shortcodes'], function ($key) {
return !in_array($key, ['{submission.created_at}', '{submission.status}']);
}, ARRAY_FILTER_USE_KEY);
$submissionCodes = $this->formatAdvancedFilters($submissionCodes);
$allCodes = array_merge($formShortCodes, $userCodes, $submissionCodes);
$groups = [];
foreach ($allCodes as $code) {
$groups[] = $code;
}
$data['advanced_filters'] = array_values($groups);
$data['advanced_filters_operators'] = $this->getOperators();
$data['advanced_filters_columns'] = [
'numeric' => [
'user.ID', 'entry-attribute.id', 'entry-attribute.serial_number', 'entry-attribute.user_id',
]
];
return $data;
}
protected function formatAdvancedFilters($shortCodes)
{
$codes = [];
foreach ($shortCodes['shortcodes'] as $code => $label) {
preg_match('/{+(.*?)}/', $code, $matches);
if ($matches && false !== strpos($matches[1], 'submission.')) {
$code = substr($matches[1], strlen('submission.'));
}
$codes[] = [
'label' => $label,
'value' => $code,
];
}
return [
$shortCodes['value'] => [
'label' => $shortCodes['title'],
'value' => $shortCodes['value'],
'children' => $codes
]
];
}
protected function getUserCodes()
{
return [
'ID' => "ID",
'user_login' => "Username",
'display_name' => 'Display Name',
'user_email' => 'Email'
];
}
public function applyAdvancedFilter($query, $attributes)
{
// Prepare filter groups
$advanceFilters = Arr::get($attributes, 'advanced_filter');
$formId = Arr::get($attributes, 'form_id');
$form = Helper::getForm($formId);
if (!$form) {
return $query;
}
$this->setSupportedColumns($form);
$filters = $this->formatAndSanitizeFilters($advanceFilters);
if (!$filters) {
return $query;
}
return $this->applyFiltersQuery($query, $filters);
}
protected function setSupportedColumns($form)
{
$data = $this->getAdvancedFilterOptions([], $form);
$filters = Arr::get($data, 'advanced_filters');
$columns = [];
foreach ($filters as $filter) {
$children = Arr::get($filter, 'children');
$columns = array_merge($columns, array_column($children, null, 'value'));
}
$this->supportedColumns = $columns;
$this->numericColumns = Arr::get($data, 'advanced_filters_columns.numeric');
}
protected function formatAndSanitizeFilters($filters)
{
// Use default filters if not has a valid filter
if (empty($filters)) {
return false;
}
$formattedFilters = [];
foreach ($filters as $groupsIndex => $groups) {
if (!is_array($groups)) {
continue;
}
foreach ($groups as $group) {
if ($group = $this->sanitizeFilterGroup($group)) {
$sourceType = Arr::get($group, '0.0');
$formattedFilters[$groupsIndex][$sourceType][] = $group;
}
}
}
return $formattedFilters;
}
protected function sanitizeFilterGroup($group)
{
if (!$this->isValidGroup($group)) {
return false;
}
$source = Arr::get($group, 'source');
$operator = Arr::get($group, 'operator');
$value = Arr::get($group, 'value');
$value = $this->sanitizeValueBySource($value, $source, $operator);
if (null === $value || (is_array($value) && empty($value))) {
return false;
}
return $this->sanitizeFilterGroupForEluquentModel($source, $operator, $value);
}
protected function sanitizeValueBySource($value, $source, $operator)
{
$callback = null;
$column = join('.', $source);
if (in_array($column, $this->numericColumns)) {
$callback = 'absint';
} elseif ('entry-attribute.is_favourite' == $column) {
$value = (int)Arr::isTrue(['is_favorites' => $value],'is_favorites');
}
return $this->sanitizeValue($value, $callback, $operator);
}
protected function sanitizeValue($value, $callback, $operator)
{
if (null === $value) {
return null;
}
$value = $this->resolveValueForSanitize($value, $operator);
if (is_array($value)) {
if (is_callable($callback)) {
$value = array_filter(array_map($callback, $value));
} else {
$value = array_filter(array_map(function ($v) {
return $this->sanitizeWithWPDB($v);
}, $value));
}
} elseif (is_callable($callback)) {
$value = $callback($value);
} else {
$value = $this->sanitizeWithWPDB($value);
}
return $value;
}
protected function resolveValueForSanitize($value, $operator)
{
switch ($operator) {
case 'IN':
case 'NOT IN':
if (is_string($value)) {
$value = explode(',', $value);
}
$value = array_map('trim', $value);
break;
default:
break;
}
return $value;
}
protected function sanitizeWithWPDB($value)
{
global $wpdb;
$value = $wpdb->prepare('%s', $value);
if ($value) {
// Trim leading and trailing quotes
$value = trim($value, "'\"");
}
return $value;
}
protected function sanitizeFilterGroupForEluquentModel($source, $operator, $value)
{
global $wpdb;
switch ($operator) {
case 'contains':
case 'doNotContains':
case 'startsWith':
case 'endsWith':
if (is_array($value)) {
$value = join('', $value);
}
$value = $wpdb->esc_like($value);
if ('startsWith' === $operator) {
$value = "%" . $value;
} elseif ('endsWith' === $operator) {
$value = $value . "%";
} else {
$value = "%" . $value . "%";
}
$operator = 'doNotContains' === $operator ? 'NOT LIKE' : 'LIKE';
break;
default:
break;
}
return [$source, $operator, $value];
}
protected function isValidGroup($group)
{
if (!$this->isSupportedColumn(Arr::get($group, 'source.1'))) {
return false;
}
if (!$this->isValidOperator(Arr::get($group, 'operator'))) {
return false;
}
return true;
}
protected function isValidOperator($operator)
{
return Arr::exists($this->getOperators(), $operator);
}
protected function isSupportedColumn($column)
{
return Arr::exists($this->supportedColumns, $column);
}
protected function applyFiltersQuery($query, $filters)
{
$entryDetailsJoinCount = 0;
$hasUserJoin = false;
// Determine the number of entry details joins required
foreach ($filters as $groups) {
if ($inputsConditions = Arr::get($groups, 'inputs')) {
if ($entryDetailsJoinCount < count($inputsConditions)) {
$entryDetailsJoinCount = count($inputsConditions);
}
}
if (Arr::isTrue($groups, 'user')) {
$hasUserJoin = true;
}
}
// Apply the necessary joins
for ($i = 0; $i < $entryDetailsJoinCount; $i++) {
$entryDetailsTableAlies = 'entry_details_' . $i;
$this->entryDetailsTableAlies[] = $entryDetailsTableAlies;
$query->leftJoin("fluentform_entry_details as {$entryDetailsTableAlies}", "{$entryDetailsTableAlies}.submission_id", '=', 'fluentform_submissions.id');
}
if ($hasUserJoin) {
$query->leftJoin('users', 'users.ID', '=', 'fluentform_submissions.user_id');
}
// Apply filters
foreach ($filters as $index => $groups) {
$method = 0 === $index ? 'where' : 'orWhere';
$query->{$method}(function ($query) use ($groups) {
if ($inputsConditions = Arr::get($groups, 'inputs')) {
$this->applyWhereConditions($query, $inputsConditions, 'entryDetails');
}
if ($userConditions = Arr::get($groups, 'user')) {
$this->applyWhereConditions($query, $userConditions, 'user');
}
if ($entryAttrConditions = Arr::get($groups, 'entry-attribute')) {
$this->applyWhereConditions($query, $entryAttrConditions);
}
return $query;
});
}
$query->distinct()->select('fluentform_submissions.*');
return $query;
}
protected function applyWhereConditions($query, $conditions, $relationship = null)
{
if ($relationship) {
foreach ($conditions as $index => $condition) {
list($source, $operator, $value) = $condition;
$column = $source[1];
if ($relationship === 'entryDetails') {
// Resolve entry details join alies prefix
if ($entryDetailsTableAlias = Arr::get($this->entryDetailsTableAlies, $index)) {
$this->applyEntryDetailsQuery($query, $column, $operator, $value, $entryDetailsTableAlias);
}
} elseif ($relationship === 'user') {
$this->applyCondition($query, 'users.' . $column, $operator, $value);
}
}
} else {
foreach ($conditions as $condition) {
list($source, $operator, $value) = $condition;
$column = 'fluentform_submissions.' . $source[1];
$this->applyCondition($query, $column, $operator, $value);
}
}
}
protected function applyEntryDetailsQuery($query, $column, $operator, $value, $entryDetailsTableAlias)
{
foreach ($this->buildEntryDetailsWheres($column, $operator, $value) as $where) {
if (Arr::get($where, 'format')) {
$this->filterDates($query, $where, $entryDetailsTableAlias);
continue;
}
$_column = Arr::get($where, 'column');
$_value = Arr::get($where, 'value');
$_operator = Arr::get($where, 'operator');
$this->applyCondition($query, "{$entryDetailsTableAlias}.{$_column}", $_operator, $_value);
}
}
protected function buildEntryDetailsWheres($column, $operator, $value)
{
$valueWhere = [
'column' => 'field_value',
'operator' => $operator,
'value' => $value
];
if ($field = Arr::get($this->supportedColumns, $column)) {
if ($format = Arr::get($field, 'format')) {
$dateInfo = [
'format' => $format,
'type' => Arr::get($field, 'type'),
];
$valueWhere = array_merge($valueWhere, $dateInfo);
}
}
$wheresGroup = [
[
'column' => 'field_name',
'operator' => '=',
'value' => $column,
],
$valueWhere
];
// Resolve sub_field_name
if (preg_match('/\[(.*?)\]/', $column, $matches)) {
$wheresGroup[] = [
'column' => 'sub_field_name',
'operator' => '=',
'value' => $matches[1],
];
$wheresGroup[0]['value'] = preg_replace('/\[(.*?)\]/', '', $column);
}
return $wheresGroup;
}
function applyCondition($query, $column, $operator, $value)
{
if (is_array($value)) {
if ('IN' === $operator) {
$query->whereIn($column, $value);
} elseif ('NOT IN' === $operator) {
$query->whereNotIn($column, $value);
} else {
foreach ($value as $v) {
$query->where($column, $operator, $v);
}
}
} else {
if (in_array($operator, ['=', '!=', '<', '>', '<=', '>=']) && is_numeric($value)) {
global $wpdb;
$column = $wpdb->prefix . $column;
$query->where(function ($query) use ($column) {
$query->whereRaw("{$column} REGEXP ?", ['^-?[0-9]+(\.[0-9]+)?$']);
})->where(function ($query) use ($column, $value, $operator) {
$query->whereRaw("CAST({$column} AS DOUBLE) {$operator} ?", [$value]);
});
} else {
$query->where($column, $operator, $value);
}
}
}
protected function filterDates($query, $where, $entryDetailsTableAlias)
{
global $wpdb;
$entryDetailsTableAlias = $wpdb->prefix . $entryDetailsTableAlias;
$dateFormats = $this->getDatesInfo();
$format = Arr::get($where, 'format');
if (!Arr::exists($dateFormats, $format)) {
return $query;
}
$column = Arr::get($where, 'column');
$column = "{$entryDetailsTableAlias}.{$column}";
$operator = Arr::get($where, 'operator');
$value = Arr::get($where, 'value');
$regex = Arr::get($dateFormats, $format . '.regex');
$mysqlDateFormat = Arr::get($dateFormats, $format . '.mysql_format');
$dateInputFormat = 'time' === Arr::get($where, 'type') ? '%H:%i:%s' : '%Y-%m-%d %H:%i:%s';
$query->where(function ($query) use ($column, $mysqlDateFormat, $regex, $dateInputFormat, $operator, $value) {
// Use REGEXP to filter valid date/time strings
$query->whereRaw("{$column} REGEXP ?", [$regex]);
if (is_array($value) && count($value) === 2) {
// Use BETWEEN or NOT BETWEEN for date ranges
if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') {
$query->whereRaw(
"STR_TO_DATE({$column}, '{$mysqlDateFormat}') {$operator} STR_TO_DATE(?, '{$dateInputFormat}') AND STR_TO_DATE(?, '{$dateInputFormat}')",
[$value[0], $value[1]]
);
}
} elseif (is_string($value)) {
// Use regular comparison for single date value
$query->whereRaw(
"STR_TO_DATE({$column}, '{$mysqlDateFormat}') {$operator} STR_TO_DATE(?, '{$dateInputFormat}')",
[$value]
);
}
});
return $query;
}
public function getOperators()
{
return [
'=' => __('Equal', 'fluentformpro'),
'!=' => __('Not Equal', 'fluentformpro'),
'IN' => __('Equal In', 'fluentformpro'),
'NOT IN' => __('Not Equal In', 'fluentformpro'),
'>' => __('Greater Than', 'fluentformpro'),
'<' => __('Less Than', 'fluentformpro'),
'>=' => __('Greater Than or Equal', 'fluentformpro'),
'<=' => __('Less Than or Equal', 'fluentformpro'),
'contains' => __('Contains', 'fluentformpro'),
'doNotContains' => __('Does Not Contain', 'fluentformpro'),
'startsWith' => __('Starts With', 'fluentformpro'),
'endsWith' => __('Ends With', 'fluentformpro'),
'BETWEEN' => __('Between', 'fluentformpro'),
'NOT BETWEEN' => __('Not Between', 'fluentformpro'),
];
}
protected function getDatesInfo()
{
return [
'd/m/Y' => [
'regex' => '^\d{2}/\d{2}/\d{4}$',
'mysql_format' => '%d/%m/%Y',
'type' => 'date'
],
'm/d/Y' => [
'regex' => '^\d{2}/\d{2}/\d{4}$',
'mysql_format' => '%m/%d/%Y',
'type' => 'date'
],
'd.m.Y' => [
'regex' => '^\d{2}\.\d{2}\.\d{4}$',
'mysql_format' => '%d.%m.%Y',
'type' => 'date'
],
'm.d.Y' => [
'regex' => '^\d{2}\.\d{2}\.\d{4}$',
'mysql_format' => '%m.%d.%Y',
'type' => 'date'
],
'n/j/Y' => [
'regex' => '^\d{1,2}/\d{1,2}/\d{4}$',
'mysql_format' => '%c/%e/%Y',
'type' => 'date'
],
'm/d/y' => [
'regex' => '^\d{2}/\d{2}/\d{2}$',
'mysql_format' => '%m/%d/%y',
'type' => 'date'
],
'd/m/y' => [
'regex' => '^\d{2}/\d{2}/\d{2}$',
'mysql_format' => '%d/%m/%y',
'type' => 'date'
],
'M/d/Y' => [
'regex' => '^[A-Za-z]{3}/\d{2}/\d{4}$',
'mysql_format' => '%b/%d/%Y',
'type' => 'date'
],
'y/m/d' => [
'regex' => '^\d{2}/\d{2}/\d{2}$',
'mysql_format' => '%y/%m/%d',
'type' => 'date'
],
'Y-m-d' => [
'regex' => '^\d{4}-\d{2}-\d{2}$',
'mysql_format' => '%Y-%m-%d',
'type' => 'date'
],
'd-M-y' => [
'regex' => '^\d{2}-[A-Za-z]{3}-\d{2}$',
'mysql_format' => '%d-%b-%y',
'type' => 'date'
],
'm/d/Y h:i K' => [
'regex' => '^\d{2}/\d{2}/\d{4} \d{1,2}:\d{2} [APap][Mm]$',
'mysql_format' => '%m/%d/%Y %l:%i %p',
'type' => 'datetime'
],
'm/d/Y H:i' => [
'regex' => '^\d{2}/\d{2}/\d{4} \d{2}:\d{2}$',
'mysql_format' => '%m/%d/%Y %H:%i',
'type' => 'datetime'
],
'd/m/Y h:i K' => [
'regex' => '^\d{2}/\d{2}/\d{4} \d{1,2}:\d{2} [APap][Mm]$',
'mysql_format' => '%d/%m/%Y %l:%i %p',
'type' => 'datetime'
],
'd/m/Y H:i' => [
'regex' => '^\d{2}/\d{2}/\d{4} \d{2}:\d{2}$',
'mysql_format' => '%d/%m/%Y %H:%i',
'type' => 'datetime'
],
'd.m.Y h:i K' => [
'regex' => '^\d{2}\.\d{2}\.\d{4} \d{1,2}:\d{2} [APap][Mm]$',
'mysql_format' => '%d.%m.%Y %l:%i %p',
'type' => 'datetime'
],
'd.m.Y H:i' => [
'regex' => '^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}$',
'mysql_format' => '%d.%m.%Y %H:%i',
'type' => 'datetime'
],
'h:i K' => [
'regex' => '^\d{1,2}:\d{2} [APap][Mm]$',
'mysql_format' => '%l:%i %p',
'type' => 'time'
],
'H:i' => [
'regex' => '^\d{2}:\d{2}$',
'mysql_format' => '%H:%i',
'type' => 'time'
],
];
}
protected function supportedFields()
{
return [
"input_name",
"input_email",
"input_text",
"input_mask",
"textarea",
"address",
"input_number",
"select",
"input_radio",
"input_checkbox",
"multi_select",
"input_url",
"input_date",
"select_country",
"custom_html",
"ratings",
"input_hidden",
"terms_and_condition",
"gdpr_agreement",
'custom_payment_component',
'multi_payment_component',
'payment_method',
'item_quantity_component',
'rangeslider',
'payment_coupon',
];
}
}