fse = Container::instance()->getFilterService(); $this->em = Container::instance()->getEntityManager(); $this->setupDefaultFields(); } public function registerHooks() { if ( ! $this->hooksRegistered ) { add_filter( 'wpc_input_type_select', [ $this, 'addSpinnerToSelect' ], 10, 2 ); add_filter( 'wpc_input_type_radio', [ $this, 'addSpinnerToDateFormats' ], 10, 2 ); add_action( 'wp_ajax_wpc-delete-filter', [ $this, 'ajaxDeleteFilter' ] ); add_action( 'wp_ajax_wpc-load-exclude-terms', [ $this, 'sendExcludedTerms' ] ); add_action( 'wp_ajax_wpc_get_date_formats', [ $this, 'sendDateFormats' ] ); add_action( 'wp_ajax_wpc-validate-filters', [ $this, 'ajaxValidateFilters' ] ); add_action( 'after_delete_post', [ $this, 'deleteRelatedFilters' ], 10, 2 ); $this->hooksRegistered = true; } } private function setupDefaultFields() { // maybe add filter in future to allow change default fields do_action( 'wpc_before_setup_filter_fields' ); $entity = $this->em->getEntityByFilter( array('entity' => 'taxonomy', 'e_name' => 'category'), 'post' ); $entityTerms = $entity->getTermsForSelect(); asort( $entityTerms, SORT_NATURAL ); $defaultFields = array( 'ID' => array( 'type' => 'Hidden' ), 'parent' => array( 'type' => 'Hidden' ), 'menu_order' => array( 'type' => 'Hidden', 'class' => 'wpc-menu-order-field' ), 'entity' => array( 'type' => 'Select', 'label' => esc_html__( 'Filter by', 'filter-everything' ), 'class' => 'wpc-field-entity', 'options' => $this->em->getPossibleEntities(), 'default' => 'taxonomy_category', 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything' ), 'required' => true ), 'e_name' => array( 'type' => 'Text', 'label' => esc_html__( 'Meta Key', 'filter-everything' ), 'class' => 'wpc-field-ename', 'instructions' => esc_html__( 'Name of the Custom Field', 'filter-everything'), 'required' => true ), 'label' => array( 'type' => 'Text', 'label' => esc_html__( 'Filter Title', 'filter-everything' ), 'class' => 'wpc-field-label', 'placeholder' => esc_html__( 'New Filter', 'filter-everything'), 'instructions' => esc_html__( 'Will appear in the Filters widget', 'filter-everything' ) ), 'slug' => array( 'type' => 'Text', 'label' => esc_html__( 'Var Name for URL', 'filter-everything' ), 'class' => 'wpc-field-slug', 'instructions' => esc_html__( 'Name of a part of the URL responsible for this filter', 'filter-everything'), 'tooltip' => wp_kses( __( 'For example, in the URL path:/?color=blue&size=large"color" and "size" are the names of the URL vars.In the PRO version of the plugin, the URL part after "?" becomes /color-blue/size-large/', 'filter-everything'), array( 'br' => array() ) ), 'required' => true ), // Optional 'view' => array( 'type' => 'Select', 'label' => esc_html__( 'View in Widget', 'filter-everything' ), 'class' => 'wpc-field-view', 'options' => $this->getViewOptions(), 'default' => 'checkboxes', 'instructions' => '', ), 'show_range_list' => array( 'type' => 'inProButton', 'pro_label' => flrt_pro_promo_label(), 'label' => esc_html__('Show Range List', 'filter-everything'), 'class' => 'wpc-field-show-range-list', 'default' => '', 'tooltip' => '', ), 'dropdown_label' => array( 'type' => 'Text', 'label' => esc_html__( 'Dropdown Label', 'filter-everything' ), 'class' => 'wpc-field-dropdown-label', 'default' => '', 'placeholder' => esc_html__( 'e.g. - Select color -', 'filter-everything' ), 'instructions' => esc_html__( 'Label for the default dropdown option', 'filter-everything' ), ), 'date_type' => array( 'type' => 'Select', 'label' => esc_html__( 'Date Type', 'filter-everything' ), 'class' => 'wpc-date-type', 'options' => array( 'date' => esc_html__( 'Date', 'filter-everything' ), 'datetime' => esc_html__( 'Date Time', 'filter-everything' ), 'time' => esc_html__( 'Time', 'filter-everything' ), ), 'default' => 'date', 'instructions' => '', ), 'date_format' => array( 'type' => 'Radio', 'label' => esc_html__( 'Date Format', 'filter-everything' ), 'class' => 'wpc-date-format', 'options' => $this->getDateFormatOptions(), 'default' => __( 'F j, Y' ), 'instructions' => esc_html__( 'How the date will be displayed in the Filters widget', 'filter-everything' ), 'tooltip' => wp_kses( sprintf( __( 'More PHP date formats can be found on this page.', 'filter-everything' ), 'https://wordpress.org/documentation/article/customize-date-and-time-format/' ), array( 'a' => array( 'href' => true, ) ) ) ), 'show_term_names' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Show Term names', 'filter-everything' ), 'class' => 'wpc-field-show-term-names', 'default' => 'yes', 'instructions' => esc_html__( 'Term names for filters with swatches or brand images may be hidden', 'filter-everything' ), 'tooltip' => '', ), 'selected_and_above' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Mode «All stars+»', 'filter-everything' ), 'class' => 'wpc-field-show-selected-and-above', 'default' => 'no', 'instructions' => esc_html__( 'E.g. if 3 stars are selected, all products with rating 3+ will be shown', 'filter-everything' ), 'tooltip' => '', ), 'logic' => array( 'type' => 'Select', 'label' => esc_html__( 'Filter Logic', 'filter-everything' ), 'class' => 'wpc-field-logic', 'options' => array('or' => esc_html__('OR', 'filter-everything' ), 'and' => esc_html__('AND', 'filter-everything') ), 'default' => 'or', 'instructions' => esc_html__( 'Determines how to select posts when two or more terms of this filter are selected', 'filter-everything' ), 'tooltip' => wp_kses( __( '«OR» means to show posts if they are at least in one of the selected terms. «AND» means that posts should belong to all selected terms at the same time.', 'filter-everything' ), array( 'br' => array() ) ), ), // AND 'orderby' => array( 'type' => 'Select', 'label' => esc_html__( 'Sort Terms by', 'filter-everything' ), 'class' => 'wpc-field-orderby', 'options' => $this->getOrderByOptions(), 'default' => 'default', 'instructions' => esc_html__('The order in which terms appear in the widget', 'filter-everything'), 'tooltip' => wp_kses( __( 'The "Default" option means that terms will be sorted in the order as they were received from the DatabaseThe "Menu order" option is available for WooCommerce attributes only.', 'filter-everything' ), array( 'br' => array() ) ) ), 'in_path' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'In URL', 'filter-everything' ), 'class' => 'wpc-field-path', 'default' => 'yes', 'instructions' => '' ), 'exclude' => array( 'type' => 'Select', 'label' => esc_html__( 'Include/Exclude Terms', 'filter-everything' ), 'class' => 'wpc-field-exclude', 'options' => $entityTerms, //$entity->getTermsForSelect(), 'multiple' => 'multiple', 'instructions' => '', 'skip_view' => true ), 'include' => array( 'type' => 'Select', 'label' => '', 'class' => 'wpc-field-include', 'options' => [ 'no' => esc_html__( 'exclude', 'filter-everything' ), 'yes' => esc_html__('include', 'filter-everything' ) ], 'default' => 'no', 'instructions' => esc_html__( 'Include selected terms only', 'filter-everything' ), 'skip_view' => true ), 'collapse' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Folding', 'filter-everything' ), 'class' => 'wpc-field-collapse', 'default' => 'no', 'instructions' => esc_html__( 'Makes filter collapsible in the widget', 'filter-everything'), 'tooltip' => esc_html__( 'Useful in situations when the filter is rarely applied but takes up some space in the widget. Collapsed by default.', 'filter-everything' ) ), 'hierarchy' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Show Hierarchy', 'filter-everything' ), 'class' => 'wpc-field-hierarchy', 'default' => 'no', 'instructions' => esc_html__( 'Display the term hierarchy in the filter widget. Child terms will be collapsed by default', 'filter-everything' ) ), 'range_slider' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Enable Range Slider?', 'filter-everything' ), 'class' => 'wpc-field-range-slider', 'default' => 'yes', 'instructions' => esc_html__( 'If disabled, visitors must type numeric values in text inputs', 'filter-everything' ) ), 'step' => array( 'type' => 'Text', 'label' => esc_html__( 'Slider Step', 'filter-everything' ), 'class' => 'wpc-field-value-step', 'instructions' => esc_html__( 'Determines how the numeric value will be changed when you move slider controls', 'filter-everything' ), 'tooltip' => wp_kses( __('Step 1 means possible values are 1,2,3,4 ...Step 0.1 means possible values are 5.1, 5.2, 5.3, 5.4 ...Step 15 means possible values are 15, 30, 45, 60...', 'filter-everything'), array( 'br' => array() ) ), 'default' => 1 ), 'search' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Terms Search', 'filter-everything' ), 'class' => 'wpc-field-search', 'default' => 'no', 'instructions' => esc_html__( 'Adds a search field that allows you to quickly find filter terms', 'filter-everything' ) ), 'parent_filter' => array( 'type' => 'Select', 'label' => esc_html__( 'Parent Filter', 'filter-everything' ), 'class' => 'wpc-field-parent-filter', 'options' => [ '-1' => esc_html__( 'Please, add filters first', 'filter-everything' ) ], 'instructions' => esc_html__( 'If specified, current Filter terms become available only after the parent Filter selected', 'filter-everything' ) ), 'hide_until_parent' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Hide until the Parent selected', 'filter-everything' ), 'class' => 'wpc-field-hide-until-parent', 'default' => 'no', 'instructions' => esc_html__( 'Do not show this Filter until a parent is selected. Useful for step-by-step filtering', 'filter-everything' ) ), 'min_num_label' => array( 'type' => 'Text', 'label' => esc_html__( 'Labels for Chips', 'filter-everything' ), 'class' => 'wpc-field-min-num-label', 'default' => '', 'placeholder' => esc_html__( 'e.g. From {value}', 'filter-everything' ), 'instructions' => '', 'skip_view' => true, ), 'max_num_label' => array( 'type' => 'Text', 'label' => '', 'class' => 'wpc-field-max-num-label', 'default' => '', 'placeholder' => esc_html__( 'e.g. To {value}', 'filter-everything' ), 'instructions' => '', 'skip_view' => true, ), 'tooltip' => array( 'type' => 'Text', 'label' => esc_html__( 'Tooltip', 'filter-everything' ), 'class' => 'wpc-field-tooltip', 'instructions' => esc_html__( 'Will appear next to the Filter Title', 'filter-everything' ), 'default' => '' ), 'more_less' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'More/Less', 'filter-everything' ), 'class' => 'wpc-field-more-less', 'default' => 'no', 'instructions' => sprintf( esc_html__( 'Show a toggle link after %s terms', 'filter-everything' ), flrt_more_less_count() ) ), 'show_chips' => array( 'type' => 'Checkbox', 'label' => esc_html__( 'Show in Chips', 'filter-everything' ), 'class' => 'wpc-field-show-chips', 'default' => 'yes', 'instructions' => esc_html__( 'Show filter selected terms in the list of all chosen items', 'filter-everything' ) ), 'acf_fields' => array( 'type' => 'Text', 'label' => esc_html__( 'ACF Fields', 'filter-everything' ), 'class' => 'wpc-field-acf', 'default' => '', 'instructions' => esc_html__( 'Contains the IDs of ACF fields that are related to the Custom Field', 'filter-everything' ) ) ); $this->defaultFields = apply_filters( 'wpc_filter_default_fields', $defaultFields, $this ); } public static function getViewOptions() { $viewOptions = array( 'checkboxes' => esc_html__('Checkboxes', 'filter-everything'), 'radio' => esc_html__('Radio buttons', 'filter-everything'), 'labels' => esc_html__('Labels list', 'filter-everything'), 'dropdown' => esc_html__('Dropdown', 'filter-everything'), 'range' => esc_html__('Numeric range', 'filter-everything'), 'date' => esc_html__('Date range', 'filter-everything'), ); if(flrt_is_woocommerce()){ $viewOptions['rating'] = esc_html__('Rating', 'filter-everything'); } return $viewOptions; } public static function getDateFormatOptions( $type = 'date' ) { switch( $type ){ case 'date': $F_j_Y = date_i18n( __( 'F j, Y' ) ); $d_m_Y = date_i18n( 'd/m/Y' ); $m_d_Y = date_i18n( 'm/d/Y' ); $formatOptions = array( __( 'F j, Y' ) => '' . $F_j_Y . ''.__( 'F j, Y' ).'', 'd/m/Y' => '' . $d_m_Y . 'd/m/Y', 'm/d/Y' => '' . $m_d_Y . 'm/d/Y', ); break; case 'datetime': $F_j_Y = date_i18n( __('F j, Y g:i a') ); $d_m_Y = date_i18n( 'd/m/Y g:i a' ); $m_d_Y = date_i18n( 'm/d/Y g:i a' ); $formatOptions = array( __('F j, Y g:i a') => '' . $F_j_Y . ''.__('F j, Y g:i a').'', 'd/m/Y g:i a' => '' . $d_m_Y . 'd/m/Y g:i a', 'm/d/Y g:i a' => '' . $m_d_Y . 'm/d/Y g:i a', ); break; case 'time': $g_i_a = date_i18n( __('g:i a') ); $H_i_s = date_i18n( 'H:i:s' ); $formatOptions = array( __('g:i a') => '' . $g_i_a . ''.__('g:i a').'', 'H:i:s' => '' . $H_i_s . 'H:i:s', ); break; } $formatOptions['other'] = '' . esc_html__( 'Custom:', 'filter-everything' ) . ''; return $formatOptions; } public function getOrderByOptions() { $orderBy = array( 'default' => esc_html__( 'Default (no sorting)', 'filter-everything' ), 'nameasc' => esc_html__( 'Term name «abc»', 'filter-everything' ), 'postcountasc' => esc_html__( 'Posts count «123»', 'filter-everything' ), 'idasc' => esc_html__( 'Term ID «123»', 'filter-everything' ), 'menuasc' => esc_html__( 'Menu order «123»', 'filter-everything' ), 'namedesc' => esc_html__( 'Term name «cba»', 'filter-everything' ), 'postcountdesc' => esc_html__( 'Posts count «321»', 'filter-everything' ), 'iddesc' => esc_html__( 'Term ID «321»', 'filter-everything' ), 'menudesc' => esc_html__( 'Menu order «321»', 'filter-everything' ) ); return $orderBy; } public function getFieldsByType($type, $configuredFields = [] ) { $selected = []; $type = ucfirst($type); foreach ( $configuredFields as $key => $field) { if ( isset( $field['type'] ) && ( $field['type'] === $type ) ) { $selected[$key] = $field; } } return $selected; } private function getFilterNames( $set_id ) { $filternames = []; if( ! $set_id ){ return $filternames; } $filters = $this->em->selectOnlySetFilters( $set_id ); if( ! empty( $filters ) ) { if ( count( $filters ) > 1 ) { $filternames['-1'] = esc_html__( '— Select Filter —', 'filter-everything' ) ; } else { $filternames['-1'] = esc_html__( 'Please, add filters first', 'filter-everything' ); } foreach ( $filters as $filter ) { if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) { continue; } if ( isset( $filter['ID'] ) ) { $filternames[ $filter['ID'] ] = $filter['label'] .' (' . $filter['e_name'] .')'; } } } return $filternames; } public function getEmptyFilterObject( $set_id ) { $defaults = new \stdClass(); $defaults->ID = self::FLRT_NEW_FILTER_ID; $defaults->post_parent = ''; $defaults->menu_order = 0; $defaults->post_title = ''; $defaults->post_content = ''; $defaults->post_name = ''; $filter = $this->em->prepareFilter( $defaults ); return $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) ); } public function getFiltersInputs( $set_id ) { $preparedFilters = []; if( ! $set_id || empty( $set_id ) ){ return $preparedFilters; } $filters = $this->em->selectOnlySetFilters( $set_id ); foreach ( $filters as $field_key => $filter ){ $preparedFilters[] = $this->prepareFilterInputsToDisplay( $filter, $this->getFilterNames( $set_id ) ); } return $preparedFilters; } private function prepareFilterInputsToDisplay( $filter, $filternames ) { $postTypes = false; $terms = []; // Used for filter table class in filter set fields. // E.g. post_meta, taxonomy, author. $short_entity = $filter['entity'] ? $filter['entity'] : 'taxonomy'; if ( $short_entity === 'taxonomy' ) { // Add hierarchical class if ( is_taxonomy_hierarchical( $filter['e_name'] ) ) { $short_entity .= ' taxonomy-hierarchical'; } // Add product attribute class if ( strpos( $filter['e_name'], 'pa_' ) === 0 ) { $short_entity .= ' taxonomy-product-attribute'; } if ( strpos( $filter['e_name'], 'product_visibility') !== false && $filter['view'] === 'rating') { $short_entity .= ' selected-and-above-show '; } if ( flrt_get_experimental_option('use_color_swatches') === 'on' ) { $taxonomies = flrt_get_experimental_option( 'color_swatches_taxonomies', [] ); if ( in_array( $filter['e_name'], $taxonomies ) ) { $short_entity .= ' taxonomy-has-swatches'; } } } if ( in_array( $filter['e_name'], flrt_brand_filter_entities() ) ) { $short_entity .= ' wpc-filter-has-brands'; } $short_entity .= isset( $filter['view'] ) ? ' wpc-view-'.$filter['view'] : ''; $belongs = $this->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] ); $postType = ''; // For post_meta_num entity postType is critical value so we have to set it up if ( in_array( $filter['entity'], array( 'post_meta_num', 'post_meta_exists', 'post_meta_date' ) ) ) { $fss = Container::instance()->getFilterSetService(); $set = $fss->getSet( $filter['parent'] ); if( isset( $set['post_type']['value'] ) ){ $postType = $set['post_type']['value']; } } if ( in_array( $filter['entity'], array( 'post_meta_num', 'tax_numeric')) !== false && $filter['view'] === 'range' && (isset($filter['show_range_list']) && $filter['show_range_list'] === 'yes')) { $short_entity .= ' wpc-view-range-list'; } $entity = $this->em->getEntityByFilter( $filter, $postType ); if( $entity ){ $terms = $entity->getTermsForSelect(); asort( $terms, SORT_NATURAL ); } // This is required only for filter fields inputs $filter = $this->fse->combineEntityNameInFilter( $filter ); foreach( $this->getFieldsMapping() as $fieldKey => $fieldData ){ if ( isset( $filter[$fieldKey] ) ) { if ( $fieldKey === 'parent_filter' ) { // Exclude current filter if ( isset( $filternames[ $filter['ID'] ] ) ) { unset( $filternames[ $filter['ID'] ] ); } if ( ! empty( $filternames ) ) { $fieldData['options'] = $filternames; } } $default_value = isset( $fieldData['default'] ) ? $fieldData['default'] : ''; $multiple = ( $fieldKey === 'exclude' && ! in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ); $fieldData['name'] = $this->generateInputName( $filter['ID'], $fieldKey, $multiple ); $fieldData['id'] = $this->generateInputID( $filter['ID'], $fieldKey ); $fieldData['value'] = ( $filter[$fieldKey] ) ? $filter[$fieldKey] : $default_value; if( $fieldKey === 'hide_until_parent' ){ if( (int) $filter['parent_filter'] > 0 ){ $fieldData['additional_class'] = 'wpc-opened'; } } if ( $fieldKey === 'slug' && $fieldData['value'] ) { $fieldData['readonly'] = 'readonly'; } if ( $fieldKey === 'e_name' && $fieldData['value'] ) { unset( $fieldData['options'] ); $fieldData['readonly'] = 'readonly'; $fieldData['type'] = 'Text'; if ( $filter['entity'] === 'tax_numeric' ) { $fieldData['label'] = esc_html__( 'Taxonomy', 'filter-everything' ); $fieldData['instructions'] = esc_html__( 'Taxonomy with numeric values you need to filter by', 'filter-everything' ); } } if( $fieldKey === 'entity' ) { $fieldData['short_entity'] = $short_entity; if ( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) { $fieldData['entity_belongs'] = true; } else { $fieldData['entity_belongs'] = $belongs; } // Add instead-entity field if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){ /** * @feature maybe move this to separate method * @bug slug wasn't saved for new filter */ // For existing filters we need to forbid changing entity value // And we need to show input instead select $fieldData['type'] = 'Text'; // To make it compatible with Text field unset( $fieldData['options'] ); // Forbid to edit field $fieldData['readonly'] = 'readonly'; $flatEntities = $this->em->getFlatEntities(); $insteadEntityVal = isset( $flatEntities[ $fieldData['value'] ] ) ? $flatEntities[ $fieldData['value'] ] : ''; if( $fieldData['value'] === 'post_meta_exists' && ! defined('FLRT_FILTERS_PRO') ){ $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything'); } if( $fieldData['value'] === 'tax_numeric' && ! defined('FLRT_FILTERS_PRO') ){ $insteadEntityVal = esc_html__('Available in PRO', 'filter-everything'); } // Field, that will be visible instead of entity, that will be hidden. $prepared[ 'instead-entity' ] = $this->getInsteadEntityField( $filter['ID'], $insteadEntityVal ); } if ( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) { if ( ! defined('FLRT_FILTERS_PRO') ) { $fieldData['disabled'] = array( 'post_meta_exists', 'tax_numeric' ); } } } if (!defined('FLRT_FILTERS_PRO')) { if ($fieldKey === 'show_range_list') { $fieldData['value'] = ''; } } if( $fieldKey === 'exclude' ){ // We always add terms even they are empty array to fill Select2 with related terms. $fieldData['options'] = $terms; if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) { $fieldData['options'] = []; } } // Set disabled fields for some situations if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) /* $filter['entity'] === 'author_author'*/ && $fieldKey === 'logic' ){ $fieldData['disabled'] = array('and'); } if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $fieldKey === 'logic' ){ $fieldData['disabled'] = array('or'); } if ( $fieldKey === 'view' ) { if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric' ] ) ) { $fieldData['disabled'] = array( 'checkboxes', 'dropdown', 'radio', 'labels', 'date', 'post_meta_date', 'colors', 'image', 'rating', ); } else if ( in_array( $filter['entity'], [ 'post_date', 'post_meta_date' ] ) ) { $fieldData['disabled'] = array( 'checkboxes', 'dropdown', 'radio', 'labels', 'range', 'colors', 'image', 'rating', ); } else if ( in_array( $filter['entity'], [ 'taxonomy_product_visibility' ] ) ) { $fieldData['disabled'] = array( 'date', 'post_meta_date', 'range', ); }else { $fieldData['disabled'] = array( 'range', 'date', 'post_meta_date', 'rating', ); } } if( $fieldKey === 'orderby' ) { if ( $filter['entity'] !== 'taxonomy_product_cat' && ( mb_strpos( $filter['entity'], 'taxonomy_pa' ) === false ) ){ $fieldData['disabled'] = ['menuasc', 'menudesc']; } } if ( ( $filter['ID'] === self::FLRT_NEW_FILTER_ID || $filter['entity'] === 'post_date' || $filter['entity'] === 'post_meta_date') && $fieldKey === 'date_format' ) { // This can be new filter or existing $date_type = $filter['date_type'] ? $filter['date_type'] : 'date'; $dateFormatOptions = $this->getDateFormatOptions( $date_type ); $disabled_custom = true; $value_to_custom = $fieldData['value']; if ( ! in_array( $value_to_custom, self::getPossibleDateFormats( $date_type ) ) ) { $disabled_custom = false; $fieldData['value'] = 'other'; } $customFormatField = '' . $this->addCustomDateFormatField( $fieldData['name'], $value_to_custom, $disabled_custom ); $dateFormatOptions['other'] = str_replace( '', '' . $customFormatField, $dateFormatOptions['other'] ); $fieldData['options'] = $dateFormatOptions; } } $prepared[ $fieldKey ] = $fieldData; } return $prepared; } public function sanitizeFilterFields( $filter ) { if( is_array( $filter ) ){ $sanitizedFilter = []; $not_esc_html = [ 'e_name', 'tooltip' ]; foreach( $filter as $key => $value ){ if( in_array( $key, $not_esc_html, true ) ){ // Why? because meta_key field can contain any different characters $sanitizedFilter[ $key ] = $value; }else{ if( is_array( $value ) ){ $sanitizedFilter[ $key ] = map_deep( $value, 'esc_html' ); } else { $sanitizedFilter[ $key ] = esc_html( $value ); } } } if( isset( $sanitizedFilter['menu_order'] ) ){ $sanitizedFilter['menu_order'] = flrt_sanitize_int( $sanitizedFilter['menu_order'] ); } if( isset( $sanitizedFilter['label'] ) ){ $sanitizedFilter['label'] = sanitize_text_field( $sanitizedFilter['label'] ); } if( isset( $sanitizedFilter['slug'] ) ){ $sanitizedFilter['slug'] = preg_replace( '/[^a-z0-9\-\_]+/', '', mb_strtolower($sanitizedFilter['slug']) ); $sanitizedFilter['slug'] = trim($sanitizedFilter['slug'], '-'); } if( isset( $sanitizedFilter['step'] ) ){ $sanitizedFilter['step'] = preg_replace('/[^\d\.]+/', '', $sanitizedFilter['step'] ); if( ! $sanitizedFilter['step'] ){ $sanitizedFilter['step'] = 1; } } if( isset( $sanitizedFilter['tooltip'] ) ){ $sanitizedFilter['tooltip'] = wp_kses( $sanitizedFilter['tooltip'], array( 'br' => array(), 'span' => array('class' => true, 'id' => true), 'em' => array(), 'strong' => array('class' => true), 'i' => array(), 'b' => array() ) ); } return $sanitizedFilter; } return $filter; } public function validateTheFilter( $filter, $id = false ) { $valid = true; $validEntity = true; $validator = new Validator(); if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ) { $filterID = $id; }else{ $filterID = $filter['ID']; } // Check all fields are our fields $defaultFields = $this->getFieldsMapping(); // To make compatible with this field $defaultFields['instead-entity'] = true; foreach( $filter as $fieldKey => $fieldValue ){ if( ! isset( $defaultFields[ $fieldKey ] ) ){ $this->pushError(32); // Invalid fields present $valid = false; } } /** * Check required field data, that should be not empty */ if( isset( $filter['ID'] ) ){ // We need to check post type for current ID otherwise we can override any existing post if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ){ $savedFilter = get_post( $filter['ID'] ); // Filter post doesn't exist if( ! $savedFilter ){ $this->pushError(33); // Invalid filter ID $valid = false; } // Other post type if( ! isset( $savedFilter->post_type ) || $savedFilter->post_type !== FLRT_FILTERS_POST_TYPE ){ $this->pushError(33); // Invalid filter ID $valid = false; } } } else { $this->pushError(33); // Invalid filter ID $valid = false; } /** * Parent field aka Set ID */ if( isset( $filter['parent'] ) ){ if( $filter['ID'] !== self::FLRT_NEW_FILTER_ID ) { $savedSet = get_post($filter['parent']); // Set post doesn't exist if (!$savedSet) { $this->pushError(34); // Invalid Set ID $valid = false; } // Other post type if ( ! isset( $savedSet->post_type ) || $savedSet->post_type !== FLRT_FILTERS_SET_POST_TYPE) { $this->pushError(34); // Invalid Set ID $valid = false; } } }else{ $this->pushError( 34); // Invalid Set ID $valid = false; } /** * Entity */ if( isset( $filter['entity'] ) ){ if( ! $validator->validatePossibleEntity( $filter['entity'] ) ){ $this->pushError( 35, $filterID, 'entity' ); // Invalid Entity $this->pushError( 35, $filterID, 'instead-entity' ); $validEntity = false; $valid = false; } }else{ $this->pushError( 35, $filterID, 'entity' ); // Invalid Entity $this->pushError( 35, $filterID, 'instead-entity' ); $validEntity = false; $valid = false; } /** * Slug validations */ if( isset( $filter['slug'] ) ){ $existingSlugs = get_option('wpc_filter_permalinks', []); $prefix = $filter['slug']; // Check this only for new filters if( $filter['ID'] === self::FLRT_NEW_FILTER_ID && $validEntity ) { $fs = Container::instance()->getFilterService(); $newEntityKey = $fs->getEntityKey($filter['entity'], $filter['e_name']); // Prohibit using the same slug for another entity if (!$validator->validateExistingPrefix($prefix, $existingSlugs, $newEntityKey)) { $this->pushError( 36, $filterID, 'slug' ); // Prefix is already used $valid = false; } } // Ensure, that prefix contains at least one alphabetic character // Also this prevents from empty prefix if ( ! $validator->validateAlphabCharsExists( $prefix ) ) { $this->pushError( 37, $filterID, 'slug' ); // Invalid prefix. Has to contain at least one alphabetic symbol $valid = false; } // Check hyphens problem when cat and cat-x exists if ( ! $validator->validatePrefixHyphens( $prefix, $existingSlugs ) ) { $this->pushError( 39, $filterID, 'slug' ); // Invalid prefix. Incorrect hyphens. $valid = false; } // Check for slugs, that matches with native WP Entities if ( ! $validator->validateAllowedPrefixes( $prefix, $filter ) ) { $errorCode = 3991; if ( defined( 'FLRT_FILTERS_PRO' ) && FLRT_FILTERS_PRO ) { $errorCode = 399; } $this->pushError( $errorCode, $filterID, 'slug' ); // Invalid prefix. Equal to wp entity. $valid = false; } } else { $this->pushError( 38, $filterID, 'slug' ); // Invalid prefix. Empty. $valid = false; } /** * E_name validations */ if ( isset( $filter['e_name'] ) ) { if( isset( $filter['entity'] ) ) { if ( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists', 'tax_numeric', 'post_meta_date' ) ) ) { $e_name = $filter['e_name']; if ( $e_name === '' ) { $this->pushError( 401, $filterID, 'e_name' ); // Select or Enter meta key. $valid = false; } else { if ( $filter['entity'] === 'tax_numeric' ) { // Validate taxonomy $taxonomies = $this->em->getPossibleTaxonomies(); $taxonomy_e_name = $filter[ 'e_name' ]; if( mb_strpos( $filter[ 'e_name' ], 'taxonomy_' ) === false ){ $taxonomy_e_name = 'taxonomy_' . $filter[ 'e_name' ]; } if ( ! isset( $taxonomies[ $taxonomy_e_name ] ) ) { $this->pushError( 402, $filterID, 'e_name' ); // Invalid E_name. $valid = false; } } else { // Should not be empty and should contain at least one alphabetic character if ( ! $validator->validateAlphabCharsExists( $e_name ) ) { $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name. $valid = false; } // Should not contain characters that escaped by esc_attr if ( ! $validator->validateEscAttrCharacters( $e_name ) ) { $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name. $valid = false; } // Some meta keys should be prohibited to use $excludedMetaKeys = flrt_get_forbidden_meta_keys(); if ( in_array( $e_name, $excludedMetaKeys, true ) ) { $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name. $valid = false; } } } } if ( in_array( $filter['e_name'], array( 'post_date') ) ) { if ( isset( $filter['date_format'] ) && isset( $filter['date_type'] ) ) { $date_time = flrt_split_date_time( $filter['date_format'] ); switch ( $filter['date_type'] ) { case 'date': // check for date if ( ! $date_time['date'] ) { $this->pushError( 60, $filterID, 'date_format' ); // Invalid date_format. $valid = false; } break; case 'datetime': if ( ! $date_time['date'] || ! $date_time['time'] ) { $this->pushError( 62, $filterID, 'date_format' ); // Invalid date_format. $valid = false; } break; case 'time': if ( ! $date_time['time'] ) { $this->pushError( 61, $filterID, 'date_format' ); // Invalid date_format. $valid = false; } break; } } } } }else{ $this->pushError( 40, $filterID, 'e_name' ); // Invalid E_name. $valid = false; } if ( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO && isset($filter['e_name'])) { if (!empty($filter['entity'])) { if (in_array($filter['entity'], array('post_meta_num', 'tax_numeric'))) { if (isset($filter['view']) && $filter['view'] === 'range' && !empty($filter['show_range_list']) && $filter['show_range_list'] === 'yes') { if (empty($filter['range_list_input'])) { $this->pushError(93, $filterID, 'range_list_input'); // Empty range_list. $valid = false; } } } } } /** * View validations */ if( isset( $filter['view'] ) ){ $viewOptions = array_keys( $this->getViewOptions() ); if( ! $validator->validateView( $filter, $viewOptions ) ){ $this->pushError( 41, $filterID, 'view' ); // Invalid View. $valid = false; } } else { $this->pushError( 41, $filterID, 'view' ); // Invalid View. $valid = false; } /** * Date Type validations */ if ( isset( $filter['date_type'] ) ) { if( ! in_array( $filter['date_type'], array( 'date', 'datetime', 'time' ), true ) ){ $this->pushError( 411, $filterID, 'date_type' ); // Invalid Data Type $valid = false; } } /** * Logic validations */ if( isset( $filter['logic'] ) ){ if( ! in_array( $filter['logic'], array( 'or', 'and' ), true ) ){ $this->pushError( 42, $filterID, 'logic' ); // Invalid Logic. $valid = false; } // For author and post_meta_exists entities logic can be only OR if( in_array( $filter['entity'], array( 'author_author', 'post_meta_exists', 'taxonomy_product_visibility' ) ) ){ if( $filter['logic'] !== 'or' ){ $this->pushError( 45, $filterID, 'logic' ); // Not acceptable logic. $valid = false; } } // For author entity logic can be only OR if ( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) { if( $filter['logic'] !== 'and' ){ $this->pushError( 47, $filterID, 'logic' ); // Not acceptable logic. $valid = false; } } } else { $this->pushError( 42, $filterID, 'logic' ); // Invalid Logic. $valid = false; } /** * Orderby validations */ if( isset( $filter['orderby'] ) ){ $orderBy = array_keys( $this->getOrderByOptions() ); if( ! in_array( $filter['orderby'], $orderBy, true ) ){ $this->pushError( 43, $filterID, 'orderby' ); // Invalid Orderby. $valid = false; } } else { $this->pushError( 43, $filterID, 'orderby' ); // Invalid Orderby. $valid = false; } /** * In path and Collapse doesn't require validations * because their values are prepended in code */ /** * Exclude validations */ if( isset( $filter['exclude'] ) && $validEntity ){ if( ! $validator->validateExcludeTerms( $filter['exclude'], $filter ) ){ $this->pushError( 44, $filterID, 'exclude' ); // Invalid Exclude. $valid = false; } } if( isset( $filter['include'] ) ){ if( ! in_array( $filter['include'], ['yes', 'no'] ) ){ $this->pushError( 441, $filterID, 'exclude' ); // Invalid Exclude. $valid = false; } } // In case when checkbox is not checked there is no $_POST['in_path'] parameter if( isset( $filter['in_path'] ) ){ if( in_array( $filter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) && $filter['in_path'] === 'yes' ){ $this->pushError( 46, $filterID, 'in_path' ); // Invalid In Path for Post meta num. $valid = false; } } // Check data types if they are correct /** * @todo validate range_slider, show_chips, step fields !!! IMPORTANT */ // Check combinations of different data /** * The unique slug and entity problem */ // The slug and entity pair should be the same between all filters in all sets! // Otherwise we will have different slugs and URLs in different categories for the same Post type. // If user adding new slug for the same entity, it should be notified, that this new slug // will be changed for all same entities. // Obligatorily sanitize all data. Maybe separate method for that. // If entity is taxonomy, field e_name should be empty. // Filter slugs should not be from blacklist. Blacklist - typical Wordpress entities /categories, tags etc // Slugs should be checked for non UTF symbols and maybe converted to latin UTF symbols // Because user can add slug = категория and also user can add slug 'cheap' where 'c' will be cyrillic or other. return $valid; } public function validateFilters( $filters ) { if ( ! $filters ) { return false; } $valid = true; // Check permissions if ( ! current_user_can( flrt_plugin_user_caps() ) ) { // An error occurred. You do not have permission to edit this. $this->pushError(202); $valid = false; } // Check for equal filters foreach ( (array) $filters as $filter ) { if( isset( $filter['entity'] ) && isset( $filter['e_name'] ) ) { // To avoid few filters with the same meta key if ( in_array( $filter['entity'], array( 'post_meta', 'post_meta_num', 'post_meta_exists'), true ) ) { $keys[] = 'post_meta' . $filter['e_name']; } elseif ( $filter['entity'] === 'tax_numeric' || $filter['entity'] === 'post_date' || $filter['entity'] === 'post_meta_date') { $keys[] = $filter['entity'] .'_'. $filter['e_name']; } else { $keys[] = $filter['entity'] . $filter['e_name']; } } } if ( flrt_array_contains_duplicate( $keys ) ) { $this->pushError(31); // Equal filters $valid = false; } return apply_filters( 'wpc_validate_filter_fields', $valid, $this ); } public function ajaxValidateFilters() { $postData = Container::instance()->getThePost(); $data = isset( $postData['validateData'] ) ? $postData['validateData'] : false; // The current admin script sends a JSON string, but a stale (cached or // un-rebuilt .min) copy still sends the bracket-serialized array — // accept both instead of fataling on stripslashes(array) under PHP 8 if ( is_array( $data ) ) { $data = wp_unslash( $data ); } elseif ( $data ) { $data = json_decode( stripslashes( $data ), true ); } if ( isset( $data['wpc_set_fields']['wp_filter_query_vars'] ) ) { $data['wpc_set_fields']['wp_filter_query_vars'] = wp_slash( $data['wpc_set_fields']['wp_filter_query_vars'] ); } $response = []; if( ! $data ){ $this->pushError(20); } if( ! isset( $data['_flrt_nonce'] ) || ! wp_verify_nonce( $data['_flrt_nonce'], FilterSet::NONCE_ACTION ) ){ $this->pushError(20); // Default common error } // Validate all filters // If no one filter it's ok if( isset( $data['wpc_filter_fields'] ) ){ $this->validateFilters( $data['wpc_filter_fields'] ); // Set up checkbox fields if they are empty $filterConfiguredFields = $this->getFieldsMapping(); // Validate each filter separately foreach ( (array) $data['wpc_filter_fields'] as $filterId => $filter ) { $filter = $this->prepareFilterCheckboxFields($filter, $this->getFieldsByType('checkbox', $filterConfiguredFields)); $this->validateTheFilter( $filter, $filterId ); } } $this->fillErrorsMessages(); $errors = $this->getErrors(); // Send errors if they exist if( $errors && ! empty( $errors ) ){ $response['errors'] = $errors; wp_send_json_error($response); } /** * @feature it is better to validate all fields and collect all errors to show them simultaneously. */ wp_send_json_success(); } function saveFilter( $filter ) { // May have been posted. Remove slashes. $filter = wp_unslash( $filter ); $filter = apply_filters( 'wpc_pre_save_filter', $filter ); unset( $filter['instead-entity'] ); // Make a backup of field data and remove some args. $_filter = $filter; flrt_extract_vars( $_filter, array( 'ID', 'label', 'parent', 'menu_order', 'slug' ) ); $_filter = $this->fse->splitEntityFullNameInFilter( $_filter ); // Create array of data to save. $to_save = array( 'ID' => $filter['ID'], 'post_status' => 'publish', 'post_type' => FLRT_FILTERS_POST_TYPE, 'post_title' => $filter['label'], 'post_content' => maybe_serialize( $_filter ), 'post_parent' => $filter['parent'], 'menu_order' => $filter['menu_order'] ? $filter['menu_order'] : 0, 'post_name' => $filter['slug'], 'post_excerpt' => $filter['entity'] ); // Unhook wp_targeted_link_rel() filter from WP 5.1 corrupting serialized data. remove_filter( 'content_save_pre', 'wp_targeted_link_rel' ); do_action( 'wpc_pre_save_post_filter', $to_save, $filter ); add_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10, 2 ); // Slash data. // WP expects all data to be slashed and will unslash it (fixes '\' character issues). $to_save = wp_slash( $to_save ); // Update or Insert. if( $filter['ID'] === self::FLRT_NEW_FILTER_ID ){ $filter['ID'] = wp_insert_post( $to_save ); }else{ wp_update_post( $to_save ); } remove_filter( 'pre_wp_unique_post_slug', 'flrt_force_non_unique_slug', 10 ); // Return field. return $filter; } public function prepareFilterCheckboxFields( $filter, $configuredFields = [] ) { foreach ( $configuredFields as $key => $checkbox ){ if( ! isset( $filter[ $key ] ) ){ $filter[ $key ] = 'no'; }else{ $filter[ $key ] = 'yes'; } } return $filter; } public function getFieldsMapping() { return $this->defaultFields; } public function deleteFilter( $ID, $force = false ) { if( ! $ID ){ return false; } if( $force ){ return wp_delete_post( $ID, true ); }else{ return wp_trash_post( $ID ); } } public function deleteRelatedFilters( $postid, $post ) { if( $post->post_type !== FLRT_FILTERS_SET_POST_TYPE ){ return $postid; } $args = array( 'post_type' => FLRT_FILTERS_POST_TYPE, 'posts_per_page' => -1, 'post_parent' => $postid, 'post_status' => array('any'), ); $setFilters = get_posts( $args ); if( ! empty( $setFilters ) ){ foreach ( $setFilters as $filter ) { $this->deleteFilter( $filter->ID, true ); } } return $postid; } public function ajaxDeleteFilter() { $postData = Container::instance()->getThePost(); $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false; if( $filterId === self::FLRT_NEW_FILTER_ID ){ wp_send_json_success(); } $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false; $errorResponse = array( 'fid' => $filterId, 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything') ); if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){ wp_send_json_error( $errorResponse ); } if( ! $filterId ){ wp_send_json_error( $errorResponse ); } $filter = get_post( $filterId ); if( ! isset( $filter->post_type ) || ( $filter->post_type !== FLRT_FILTERS_POST_TYPE ) ){ wp_send_json_error( $errorResponse ); } if( $filterPost = $this->deleteFilter( $filterId, true ) ){ $response['fid'] = $filterPost->ID; wp_send_json_success( $response ); }else{ wp_send_json_error( $errorResponse ); } } public static function getPossibleDateFormats( $date_type ) { $possibleFormats = [ 'date' => array( 'd/m/Y', 'm/d/Y', __('F j, Y'), ), 'datetime' => array( 'd/m/Y g:i a', 'm/d/Y g:i a', __('F j, Y g:i a'), ), 'time' => array( __('g:i a'), 'H:i:s', ) ]; if ( isset( $possibleFormats[$date_type] ) ) { return $possibleFormats[$date_type]; } return []; } private function addCustomDateFormatField( $name, $value, $disabled = false ) { $html = 'getThePost(); $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false; $setId = isset( $postData['setId'] ) ? $postData['setId'] : false; $dateType = isset( $postData['dateType'] ) ? $postData['dateType'] : false; $errorResponse = array( 'fid' => $filterId, 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything') ); if ( ! $filterId || ! $dateType ) { wp_send_json_error( $errorResponse ); } $response = []; // $savedValue = $this->em->getFilterBy( 'ID', $filterId, array( array( 'ID' => $setId ) ) ); $savedValue = $this->em->getFilterById( $filterId ); $formatoptions = self::getDateFormatOptions( $dateType ); $field = [ 'type' => 'Radio', 'class' => 'wpc-date-format', 'default' => __( 'F j, Y' ), ]; $default_formats = [ 'date' => __( 'F j, Y' ), 'datetime' => __('F j, Y g:i a'), 'time' => __('g:i a'), ]; $disabled_custom = true; $value_to_custom = ( ! empty( $savedValue['date_format'] ) && $dateType === ( $savedValue['date_type'] ?? '' ) ) ? $savedValue['date_format'] : $default_formats[$dateType]; if ( $value_to_custom && ! in_array( $value_to_custom, self::getPossibleDateFormats( $dateType ) ) ) { $disabled_custom = false; $field['value'] = 'other'; } $customFormatField = '' . $this->addCustomDateFormatField( $this->generateInputName( $filterId, 'date_format' ), $value_to_custom, $disabled_custom ); $formatoptions['other'] = str_replace( '', '' . $customFormatField, $formatoptions['other'] ); $field['name'] = $this->generateInputName( $filterId, 'date_format' ); $field['id'] = $this->generateInputID( $filterId, 'date_format' ); $field['options'] = $formatoptions; ob_start(); echo flrt_render_input( $field ); $response['html'] = ob_get_clean(); wp_send_json_success( $response ); die(); } public function sendExcludedTerms() { $container = Container::instance(); $postData = $container->getThePost(); $validator = new Validator(); $filterId = isset( $postData['fid'] ) ? $postData['fid'] : false; $nonce = isset( $postData['_wpnonce'] ) ? $postData['_wpnonce'] : false; $entity = isset( $postData['entity'] ) ? $postData['entity'] : false; $e_name = isset( $postData['ename'] ) ? $postData['ename'] : false; $errorResponse = array( 'fid' => $filterId, 'message' => esc_html__('An error occurred. Please, refresh the page and try again.', 'filter-everything') ); if( ! wp_verify_nonce( $nonce, FilterSet::NONCE_ACTION ) ){ wp_send_json_error( $errorResponse ); } if( ! $validator->validatePossibleEntity( $entity ) ){ wp_send_json_error( $errorResponse ); } $em = $container->getEntityManager(); if( $e_name ){ $filterEntity['entity'] = $entity; $filterEntity['e_name'] = $e_name; }else{ $fse = $container->getFilterService(); $filterEntity = $fse->splitEntityFullNameInFilter( array( 'entity' => $entity ) ); } $entity = $em->getEntityByFilter( $filterEntity ); $terms = $entity->getTermsForSelect2(); if( ! empty( $terms ) ){ $response['terms'] = $terms; $response['fid'] = $filterId; wp_send_json_success( $response ); }else{ wp_send_json_error( $errorResponse ); } } public function addSpinnerToDateFormats( $html, $attributes ) { if ( isset( $attributes['class'] ) && $attributes['class'] === 'wpc-date-format' ) { $spinner = ''."\r\n"; $openContainer = ''."\r\n"; $closeContainer = ''."\r\n"; if ( isset( $attributes['options']['d/m/Y'] ) ) { $date_type = 'wpc-type-date'; } if ( isset( $attributes['options']['d/m/Y g:i a'] ) ) { $date_type = 'wpc-type-datetime'; } if ( isset( $attributes['options']['H:i:s'] ) ) { $date_type = 'wpc-type-time'; } $html = str_replace( 'wpc-radio-list', 'wpc-radio-list ' . $date_type, $html ); $html = $spinner . $openContainer . $html . $closeContainer; } return $html; } public function addSpinnerToSelect( $html, $attributes ) { if( isset( $attributes['class'] ) ){ $requiredIds = array( $this->generateInputID( self::FLRT_NEW_FILTER_ID, 'exclude' ), ); if( in_array( $attributes['id'], $requiredIds ) ){ $spinner = ''."\r\n"; $openContainer = ''."\r\n"; $closeContainer = ''."\r\n"; $html = $spinner . $openContainer . $html . $closeContainer; } } return $html; } private function getInsteadEntityField( $filterId, $insteadEntityVal ) { $insteadEntity = array( 'type' => 'Text', 'label' => esc_html__( 'Filter by', 'filter-everything' ), 'class' => 'wpc-field-instead-entity', 'name' => $this->generateInputName( $filterId, 'instead-entity' ), 'id' => $this->generateInputID( $filterId, 'instead-entity' ), 'value' => $insteadEntityVal, 'readonly' => 'readonly', 'default' => '', 'instructions' => esc_html__( 'A thing by which posts will be filtered', 'filter-everything'), 'tooltip' => esc_html__( 'An already selected value cannot be changed. But you can always delete the current one and create a new filter if you need.', 'filter-everything' ), 'required' => true ); return $insteadEntity; } /** * @return true|false */ public function filterBelongsToPostType( $parent, $entity, $e_name ) { if( ! isset( $parent ) ){ return false; } $fss = Container::instance()->getFilterSetService(); $set = $fss->getSet( $parent ); if( ! isset( $set['post_type']['value'] ) ){ return false; } $post_type = $set['post_type']['value']; return $this->entityBelongsToPostType( $post_type, $entity, $e_name ); } /** * @return true|false */ private function entityBelongsToPostType( $post_type, $entity, $e_name ) { if ( empty( $post_type ) ){ return false; } if( in_array( $entity, array( 'author', 'post_date', 'post_meta', 'post_meta_num', 'post_meta_date', ) ) ){ return true; } if( in_array( $entity, array( 'post_meta_exists', 'tax_numeric' ) ) && defined( 'FLRT_FILTERS_PRO' ) ){ return true; } if( $entity === 'taxonomy' || $entity === 'tax_numeric' ){ if ( $entity === 'tax_numeric' ) { if ( ! defined( 'FLRT_FILTERS_PRO' ) ) { return false; } // We have to cut e_name to make correct verification if ( mb_strpos( $e_name, 'taxonomy_' ) !== false ) { $e_name = mb_strcut( $e_name, strlen( 'taxonomy_' ) ); } } return $this->isTaxonomyBelongsToPostType( $post_type, $e_name ); } return false; } public function getErrorMessage( $code = 20, $limit = null ) { $messages = self::getErrorsList($limit); if( isset( $messages[$code] ) ){ return $messages[$code]; } return $messages[20]; } public function pushError( $errorCode, $filterId = false, $filterKey = '' ) { $to_add = true; $error = array( 'code' => $errorCode ); if( $filterId && $filterKey ){ $error['id'] = $this->generateInputID( $filterId, $filterKey ); } if ( ! empty( $this->errors ) ) { foreach ( $this->errors as $single_error ) { if ( isset( $single_error['code'] ) && $single_error['code'] == $errorCode ) { $to_add = false; break; } } } if ( $to_add ) { $this->errors[] = $error; return $error; } return false; } public function getErrors() { if( ! empty( $this->errors ) ) { return $this->errors; } return false; } public function fillErrorsMessages() { if( ! empty( $this->errors ) ){ $errorsWithMessages = []; $messagesList = $this->getErrorsList(); foreach ( $this->errors as $index => $error ){ if( isset( $error['code'] ) && isset( $messagesList[ $error['code'] ] ) ){ $errorsWithMessages[$index] = $error; $errorsWithMessages[$index]['message'] = $messagesList[ $error['code'] ]; } } $this->errors = $errorsWithMessages; return true; } return false; } public function getErrorCodes() { $codes = []; if( ! empty( $this->errors ) ){ foreach( $this->errors as $error ){ $codes[] = $error['code']; } } return $codes; } /** * @return true|false */ private function isTaxonomyBelongsToPostType( $post_type, $taxonomy = null ) { if ( is_object( $post_type ) ) $post_type = $post_type->post_type; if ( empty( $post_type ) ){ return false; } $taxonomies = get_object_taxonomies( $post_type ); return in_array( $taxonomy, $taxonomies ); } public function generateInputID( $ID, $key ) { return self::FILTER_FIELD_KEY . '-' . $ID . '-' . $key; } public function generateInputName( $ID, $key, $multiple = false ) { $name = self::FILTER_FIELD_KEY . '['. $ID .']['. $key . ']'; if( $multiple ){ $name .= '[]'; } return $name; } public static function getErrorsList($limit = null) { $effectiveLimit = is_int($limit) ? $limit : FilterSet::FREE_LIMIT_NEW; $errors = array( // Set errors 20 => esc_html__('An error occurred. Filter Set fields were not saved, please try again.', 'filter-everything'), 201 => esc_html__('An error occurred. SEO Rule fields were not saved, please try again.', 'filter-everything'), 202 => esc_html__('An error occurred. You do not have permission to edit this.', 'filter-everything'), 211 => esc_html__( 'Error: invalid WP Page type.', 'filter-everything' ), 21 => esc_html__( 'Error: invalid post type.', 'filter-everything' ), 22 => esc_html__( 'Error: invalid value of the Where to filter? field.', 'filter-everything' ), 221 => esc_html__( 'Error: invalid query.', 'filter-everything' ), 23 => esc_html__( 'Error: invalid Hide empty field.', 'filter-everything' ), 24 => esc_html__( 'Error: invalid Show count field.', 'filter-everything' ), 25 => esc_html__( 'Error: invalid Horizontal layout field.', 'filter-everything' ), 242 => esc_html__( 'Error: invalid «Apply Button» mode field.', 'filter-everything' ), // 241 => esc_html__( 'Error: invalid Order field.', 'filter-everything' ), // Filters errors 31 => esc_html__( 'Error: two or more filters with equal Filter By and Meta key values are forbidden in the same Set. Please, remove or change equal filters.', 'filter-everything' ), 32 => esc_html__( 'Error: invalid fields present.', 'filter-everything' ), 33 => esc_html__( 'Error: invalid filter ID.', 'filter-everything' ), 34 => esc_html__( 'Error: invalid set ID.', 'filter-everything' ), 35 => esc_html__( 'Error: invalid filter entity.', 'filter-everything' ), 36 => esc_html__( 'Error: filter prefix is already used for another entity.', 'filter-everything' ), 37 => esc_html__( 'Error: filter prefix should have at least one alphabetic symbol.', 'filter-everything' ), 38 => esc_html__( 'Error: filter prefix should not be empty.', 'filter-everything' ), 39 => esc_html__( 'Error: prefix part before "-" character can not be equal to another existing prefix.', 'filter-everything' ), 399 => esc_html__( 'Error: this prefix is not allowed because it matches a taxonomy or term name already used on your site. Please use a different prefix.', 'filter-everything' ), 3991 => esc_html__( 'Error: var name is not allowed because it matches a taxonomy or term name already used on your site.', 'filter-everything' ), 40 => esc_html__( 'Error: invalid Meta key value', 'filter-everything' ), 401 => esc_html__( 'Error: you must enter Meta Key', 'filter-everything' ), 402 => esc_html__( 'Error: invalid Taxonomy', 'filter-everything' ), 41 => esc_html__( 'Error: invalid View parameter.', 'filter-everything' ), 411 => esc_html__( 'Error: invalid Date Type parameter.', 'filter-everything' ), 42 => esc_html__( 'Error: invalid Logic parameter', 'filter-everything' ), 43 => esc_html__( 'Error: invalid the Sort Terms by parameter', 'filter-everything' ), 44 => esc_html__( 'Error: invalid exclude terms', 'filter-everything' ), 441 => esc_html__( 'Error: invalid include checkbox value', 'filter-everything' ), 45 => esc_html__( 'Error: for filter Post Author logic OR is acceptable only.', 'filter-everything' ), 46 => esc_html__( 'Error: numeric filter can not be in the URL path.', 'filter-everything' ), 47 => esc_html__( 'Error: only AND logic is supported for numeric filters.', 'filter-everything' ), 48 => esc_html__( 'Error: Range slider is acceptable for Post Meta Num filters only.', 'filter-everything' ), 50 => esc_html__( 'Error: SEO Rule must have specified Post Type.', 'filter-everything' ), 51 => esc_html__( 'Error: SEO Rule must contain at least one filter.', 'filter-everything' ), 52 => esc_html__( 'Error: all SEO data fields could not be empty.', 'filter-everything' ), 53 => esc_html__( 'Error: invalid or forbidden filter presents.', 'filter-everything' ), 54 => esc_html__( 'Error: invalid SEO Rule ID.', 'filter-everything' ), 55 => esc_html__( 'Error: SEO rule with selected Filters Combination already exists.', 'filter-everything' ), 60 => esc_html__( 'Error: Invalid date format.', 'filter-everything' ), 61 => esc_html__( 'Error: Invalid time format.', 'filter-everything' ), 62 => esc_html__( 'Error: Invalid date or time format.', 'filter-everything' ), 90 => wp_kses( sprintf( __('Error: you can not update settings because the Filter Everything Pro plugin is locked. Please, enter your license key to unlock it.', 'filter-everything' ), admin_url( 'edit.php?post_type=filter-set&page=filters-settings&tab=license' ) ), array( 'a' => array( 'href'=> true, 'target' => true ) ) ), 91 => esc_html__('Error: you can not update settings because the Filter Everything Pro plugin is locked. Please, ask your site Superadmin to enter the plugin license key to unlock it.', 'filter-everything' ), 92 => wp_kses( sprintf( /* translators: 1: maximum number of free filter sets per post type, 2: PRO upgrade URL */ __('The free version allows up to %1$d filter sets per post type. Upgrade to PRO for unlimited filter sets.', 'filter-everything' ), $effectiveLimit, esc_url(FLRT_PLUGIN_URL .'/pricing/?utm_source=free_plugin&utm_medium=internal&utm_campaign=free_plugin_upgrade&utm_content=msg_three_set') ), array( 'a' => array( 'href'=> true, 'target' => true ) ) ), 93 => esc_html__('Error: The range list is empty. Please add values to the list.', 'filter-everything' ), ); return $errors; } }
'.__( 'F j, Y' ).'
d/m/Y
m/d/Y
'.__('F j, Y g:i a').'
d/m/Y g:i a
m/d/Y g:i a
'.__('g:i a').'
H:i:s