'compare' => 'NOT IN', ); } return $meta_query; } /** * Get tax query * For WooCommerce >= 3.0 * * return array */ private function getTaxQuery() { $product_visibility_term_ids = wc_get_product_visibility_term_ids(); $tax_query = array( 'relation' => 'AND', ); $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_term_ids['exclude-from-search'], 'operator' => 'NOT IN', ); // Exclude out of stock products from suggestions if ( DGWT_WCAS()->settings->getOption( 'exclude_out_of_stock' ) === 'on' ) { $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_term_ids['outofstock'], 'operator' => 'NOT IN', ); } return $tax_query; } /** * Search for matching category * * @param string $keyword * @param int $limit * * @return array */ public function getCategories( $keyword, $limit = 3 ) { $results = array( 'total' => 0, 'items' => array(), ); $args = array( 'taxonomy' => 'product_cat', ); $productCategories = get_terms( 'product_cat', apply_filters( 'dgwt/wcas/search/product_cat/args', $args ) ); $keywordUnslashed = wp_unslash( $keyword ); // Compare keyword and term name $i = 0; foreach ( $productCategories as $cat ) { if ( $i < $limit ) { $catName = html_entity_decode( $cat->name ); $pos = strpos( mb_strtolower( remove_accents( Helpers::removeGreekAccents( $catName ) ) ), mb_strtolower( remove_accents( Helpers::removeGreekAccents( $keywordUnslashed ) ) ) ); if ( $pos !== false ) { $results['total']++; $termLang = Multilingual::getTermLang( $cat->term_id, 'product_cat' ); $results['items'][$i] = array( 'term_id' => $cat->term_id, 'taxonomy' => 'product_cat', 'value' => $catName, 'url' => get_term_link( $cat, 'product_cat' ), 'breadcrumbs' => Helpers::getTermBreadcrumbs( $cat->term_id, 'product_cat', array(), $termLang, array($cat->term_id) ), 'type' => 'taxonomy', ); // Fix: Remove last separator if ( !empty( $results['items'][$i]['breadcrumbs'] ) ) { $results['items'][$i]['breadcrumbs'] = mb_substr( $results['items'][$i]['breadcrumbs'], 0, -3 ); } $i++; } } } return $results; } /** * Extend research in the Woo tags * * @param strong $keyword * @param int $limit * * @return array */ public function getTags( $keyword, $limit = 3 ) { $results = array( 'total' => 0, 'items' => array(), ); $args = array( 'taxonomy' => 'product_tag', ); $productTags = get_terms( 'product_tag', apply_filters( 'dgwt/wcas/search/product_tag/args', $args ) ); $keywordUnslashed = wp_unslash( $keyword ); // Compare keyword and term name $i = 0; foreach ( $productTags as $tag ) { if ( $i < $limit ) { $tagName = html_entity_decode( $tag->name ); $pos = strpos( mb_strtolower( remove_accents( Helpers::removeGreekAccents( $tagName ) ) ), mb_strtolower( remove_accents( Helpers::removeGreekAccents( $keywordUnslashed ) ) ) ); if ( $pos !== false ) { $results['total']++; $results['items'][$i] = array( 'term_id' => $tag->term_id, 'taxonomy' => 'product_tag', 'value' => $tagName, 'url' => get_term_link( $tag, 'product_tag' ), 'parents' => '', 'type' => 'taxonomy', ); $i++; } } } return $results; } /** * Search in extra fields * * @param string $search SQL * * @return string prepared SQL */ public function searchFilters( $search, $wp_query ) { global $wpdb; if ( empty( $search ) ) { return $search; // skip processing - there is no keyword } if ( $this->isAjaxSearch() ) { $q = $wp_query->query_vars; if ( $q['post_type'] !== 'product' ) { return $search; // skip processing } $n = ( !empty( $q['exact'] ) ? '' : '%' ); $search = $searchand = ''; if ( !empty( $q['search_terms'] ) ) { foreach ( (array) $q['search_terms'] as $term ) { $like = $n . $wpdb->esc_like( $term ) . $n; $search .= "{$searchand} ("; // Search in title if ( in_array( 'title', $this->searchIn ) ) { $search .= $wpdb->prepare( "({$wpdb->posts}.post_title LIKE %s)", $like ); } else { $search .= "(0 = 1)"; } // Search in content if ( DGWT_WCAS()->settings->getOption( 'search_in_product_content' ) === 'on' && in_array( 'content', $this->searchIn ) ) { $search .= $wpdb->prepare( " OR ({$wpdb->posts}.post_content LIKE %s)", $like ); } // Search in excerpt if ( DGWT_WCAS()->settings->getOption( 'search_in_product_excerpt' ) === 'on' && in_array( 'excerpt', $this->searchIn ) ) { $search .= $wpdb->prepare( " OR ({$wpdb->posts}.post_excerpt LIKE %s)", $like ); } // Search in SKU if ( DGWT_WCAS()->settings->getOption( 'search_in_product_sku' ) === 'on' && in_array( 'sku', $this->searchIn ) ) { $search .= $wpdb->prepare( " OR (dgwt_wcasmsku.meta_key='_sku' AND dgwt_wcasmsku.meta_value LIKE %s)", $like ); } $search = apply_filters( 'dgwt/wcas/native/search_query/search_or', $search, $like, $this ); $search .= ")"; $searchand = ' AND '; } } if ( !empty( $search ) ) { $search = " AND ({$search}) "; if ( !is_user_logged_in() ) { $search .= " AND ({$wpdb->posts}.post_password = '') "; } } } return $search; } /** * @param $where * * @return string */ public function searchDistinct( $where ) { if ( $this->isAjaxSearch() ) { return 'DISTINCT'; } return $where; } /** * Join the postmeta column in the search posts SQL */ public function searchFiltersJoin( $join, $query ) { global $wpdb; if ( empty( $query->query_vars['post_type'] ) || $query->query_vars['post_type'] !== 'product' ) { return $join; // skip processing } if ( $this->isAjaxSearch() ) { if ( DGWT_WCAS()->settings->getOption( 'search_in_product_sku' ) === 'on' && in_array( 'sku', $this->searchIn ) ) { $join .= " INNER JOIN {$wpdb->postmeta} AS dgwt_wcasmsku ON ( {$wpdb->posts}.ID = dgwt_wcasmsku.post_id )"; } $join = apply_filters( 'dgwt/wcas/native/search_query/join', $join ); } return $join; } /** * Corrects the search by excerpt if necessary. * WooCommerce adds search in excerpt by defaults and this should be corrected. * * @param string $where * * @return string * @since 1.1.4 * */ public function fixWooExcerptSearch( $where ) { global $wp_the_query; // If this is not a WC Query, do not modify the query if ( empty( $wp_the_query->query_vars['wc_query'] ) || empty( $wp_the_query->query_vars['s'] ) ) { return $where; } if ( DGWT_WCAS()->settings->getOption( 'search_in_product_excerpt' ) !== 'on' && in_array( 'excerpt', $this->searchIn ) ) { $where = preg_replace( "/OR \\(post_excerpt\\s+LIKE\\s*(\\'\\%[^\\%]+\\%\\')\\)/", "", $where ); } return $where; } /** * Disable cache results and narrowing search results to those from our engine * * @param \WP_Query $query */ public function overwriteSearchPage( $query ) { if ( !Helpers::isSearchQuery( $query ) ) { return; } if ( $this->hooked ) { return; } /** * Allowing hook WP_Query more then once * * @since 1.26.0 */ if ( apply_filters( 'dgwt/wcas/native/hook_query_once', true ) ) { $this->hooked = true; } /** * Disable cache: `cache_results` defaults to false but can be enabled */ $query->set( 'cache_results', false ); if ( !empty( $query->query['cache_results'] ) ) { $query->set( 'cache_results', true ); } $query->set( 'dgwt_wcas', $query->query_vars['s'] ); $phrase = $query->query_vars['s']; // Break early if keyword contains blacklisted phrase. if ( Helpers::phraseContainsBlacklistedTerm( $phrase ) ) { header( 'X-Robots-Tag: noindex' ); http_response_code( 400 ); exit; } $orderby = 'post__in'; $order = 'desc'; if ( !empty( $query->query_vars['orderby'] ) ) { $orderby = ( $query->query_vars['orderby'] === 'relevance' ? 'post__in' : $query->query_vars['orderby'] ); } if ( !empty( $query->query_vars['order'] ) ) { $order = strtolower( $query->query_vars['order'] ); } $postIn = array(); $searchResults = $this->getSearchResults( $phrase, true, 'product-ids' ); foreach ( $searchResults['suggestions'] as $suggestion ) { $postIn[] = $suggestion->ID; } // Integration with FiboFilters. if ( $query->get( 'fibofilters' ) ) { $postIn = array_intersect( $query->get( 'post__in' ), $postIn ); } // Save for later use $this->postsIDsBuffer = $postIn; $query->set( 'orderby', $orderby ); $query->set( 'order', $order ); $query->set( 'post__in', $postIn ); // Resetting the key 's' to disable the default search logic. $query->set( 's', '' ); } /** * Check if is ajax search processing * * @return bool * @since 1.1.3 * */ public function isAjaxSearch() { if ( defined( 'DGWT_WCAS_AJAX' ) && DGWT_WCAS_AJAX ) { return true; } return false; } /** * Headline output structure * * @return array */ public function headlineBody( $headline ) { return array( 'value' => $headline, 'type' => 'headline', ); } /** * Check if the query retuns resutls * * @return bool */ public function hasResults() { $hasResults = false; foreach ( $this->groups as $group ) { if ( !empty( $group['results'] ) ) { $hasResults = true; break; } } return $hasResults; } /** * Calc free slots * * @return int */ public function calcFreeSlots() { $slots = 0; foreach ( $this->groups as $key => $group ) { if ( !empty( $group['limit'] ) ) { $slots = $slots + absint( $group['limit'] ); } } return $slots; } /** * Apply flexible limits * * @return void */ public function applyFlexibleLimits() { $slots = $this->totalLimit; $total = 0; $groups = 0; foreach ( $this->groups as $key => $group ) { if ( !empty( $this->groups[$key]['results'] ) ) { $total = $total + count( $this->groups[$key]['results'] ); $groups++; } } $toRemove = ( $total >= $slots ? $total - $slots : 0 ); if ( $toRemove > 0 ) { for ($i = 0; $i < $toRemove; $i++) { $largestGroupCount = 0; $largestGroupKey = 'product'; foreach ( $this->groups as $key => $group ) { if ( !empty( $this->groups[$key]['results'] ) ) { $thisGroupTotal = count( $this->groups[$key]['results'] ); if ( $thisGroupTotal > $largestGroupCount ) { $largestGroupCount = $thisGroupTotal; $largestGroupKey = $key; } } } $last = count( $this->groups[$largestGroupKey]['results'] ) - 1; if ( isset( $this->groups[$largestGroupKey]['results'][$last] ) ) { unset($this->groups[$largestGroupKey]['results'][$last]); } } } } /** * Prepare suggestions based on groups * * @return array */ public function convertGroupsToSuggestions() { $suggestions = array(); $totalHeadlines = 0; foreach ( $this->groups as $key => $group ) { if ( !empty( $group['results'] ) ) { if ( $this->showHeadings ) { $suggestions[] = $this->headlineBody( $key ); $totalHeadlines++; } foreach ( $group['results'] as $result ) { $suggestions[] = $result; } } } // Remove products headline when there are only product type suggestion if ( $totalHeadlines === 1 ) { $i = 0; $unset = false; foreach ( $suggestions as $key => $suggestion ) { if ( !empty( $suggestion['type'] ) && $suggestion['type'] === 'headline' && $suggestion['value'] === 'product' ) { unset($suggestions[$i]); $unset = true; break; } $i++; } if ( $unset ) { $suggestions = array_values( $suggestions ); } } return $suggestions; } /** * Order of the search resutls groups * * @return array */ public function searchResultsGroups() { $groups = array(); if ( DGWT_WCAS()->settings->getOption( 'show_product_tax_product_cat' ) === 'on' ) { $groups['tax_product_cat'] = array( 'limit' => 3, ); } if ( DGWT_WCAS()->settings->getOption( 'show_product_tax_product_tag' ) === 'on' ) { $groups['tax_product_tag'] = array( 'limit' => 3, ); } $groups['product'] = array( 'limit' => 7, ); return apply_filters( 'dgwt/wcas/search_groups', $groups ); } /** * Allow to get the ID of products that have been found * * @param integer[] $postsIDs * * @return mixed */ public function getProductIds( $postsIDs ) { if ( $this->postsIDsBuffer !== null ) { return $this->postsIDsBuffer; } return $postsIDs; } /** * Add taxonomies labels * * @param array $labels Labels used at frontend * * @return array */ public function setTaxonomiesLabels( $labels ) { $labels['tax_product_cat_plu'] = __( 'Categories', 'woocommerce' ); $labels['tax_product_cat'] = __( 'Category', 'woocommerce' ); $labels['tax_product_tag_plu'] = __( 'Tags' ); $labels['tax_product_tag'] = __( 'Tag' ); return $labels; } /** * Backward compatibility for labels * * Full taxonomy names for categories and tags. All with prefix 'tax_'. * * @param array $labels Labels used at frontend * * @return array */ public function fixTaxonomiesLabels( $labels ) { // Product category. Old: 'category', 'product_cat_plu'. if ( isset( $labels['category'] ) ) { $labels['tax_product_cat'] = $labels['category']; unset($labels['category']); } if ( isset( $labels['product_cat_plu'] ) ) { $labels['tax_product_cat_plu'] = $labels['product_cat_plu']; unset($labels['product_cat_plu']); } // Product tag. Old: 'tag', 'product_tag_plu'. if ( isset( $labels['tag'] ) ) { $labels['tax_product_tag'] = $labels['tag']; unset($labels['tag']); } if ( isset( $labels['product_tag_plu'] ) ) { $labels['tax_product_tag_plu'] = $labels['product_tag_plu']; unset($labels['product_tag_plu']); } return $labels; } /** * Add language to WC endpoint if Polylang is active * * @param $url * @param $request * * @return string * @see polylang-wc/frontend/frontend.php:306 */ public function fixPolylangWooEndpoint( $url, $request ) { if ( PLL() instanceof \PLL_Frontend ) { // Remove wc-ajax to avoid the value %%endpoint%% to be encoded by add_query_arg (used in plain permalinks). $url = remove_query_arg( 'wc-ajax', $url ); $url = PLL()->links_model->switch_language_in_link( $url, PLL()->curlang ); return add_query_arg( 'wc-ajax', $request, $url ); } return $url; } /** * Get empty search output * * @return array */ private function getEmptyOutput() { $output = array( 'engine' => 'free', 'suggestions' => array(), 'time' => '0 sec', 'total' => 0, 'v' => DGWT_WCAS_VERSION, ); return $output; } }