'}` '#([\\{,])([\'])(\\d+|[a-z_]\\w*)\\2(?=\\:)#i', // --ibid. From `foo['bar']` to `foo.bar` '#([\\w\\)\\]])\\[([\'"])([a-z_]\\w*)\\2\\]#i', // Replace `true` with `!0` '#(?<=return |[=:,\\(\\[])true\\b#', // Replace `false` with `!1` '#(?<=return |[=:,\\(\\[])false\\b#', // Clean up ... '#\\s*(\\/\\*|\\*\\/)\\s*#', ), array( '$1', '$1$2', '}', '$1$3', '$1.$3', '!0', '!1', '$1' ), $input ); } /** * Minify CSS * * @see https://gist.github.com/tovic/d7b310dea3b33e4732c0 * * @param string * * @return string */ public static function minifyCSS( $input ) { if ( trim( $input ) === "" ) { return $input; } // Force white-space(s) in `calc()` if ( strpos( $input, 'calc(' ) !== false ) { $input = preg_replace_callback( '#(?<=[\\s:])calc\\(\\s*(.*?)\\s*\\)#', function ( $matches ) { return 'calc(' . preg_replace( '#\\s+#', "\x1a", $matches[1] ) . ')'; }, $input ); } return preg_replace( array( // Remove comment(s) '#("(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\')|\\/\\*(?!\\!)(?>.*?\\*\\/)|^\\s*|\\s*$#s', // Remove unused white-space(s) '#("(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\'|\\/\\*(?>.*?\\*\\/))|\\s*+;\\s*+(})\\s*+|\\s*+([*$~^|]?+=|[{};,>~+]|\\s*+-(?![0-9\\.])|!important\\b)\\s*+|([[(:])\\s++|\\s++([])])|\\s++(:)\\s*+(?!(?>[^{}"\']++|"(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\')*+{)|^\\s++|\\s++\\z|(\\s)\\s+#si', // Replace `0(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)` with `0` '#(?<=[\\s:])(0)(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)#si', // Replace `:0 0 0 0` with `:0` '#:(0\\s+0|0\\s+0\\s+0\\s+0)(?=[;\\}]|\\!important)#i', // Replace `background-position:0` with `background-position:0 0` '#(background-position):0(?=[;\\}])#si', // Replace `0.6` with `.6`, but only when preceded by a white-space or `=`, `:`, `,`, `(`, `-` '#(?<=[\\s=:,\\(\\-]|&\\#32;)0+\\.(\\d+)#s', // Minify string value '#(\\/\\*(?>.*?\\*\\/))|(?.*?\\*\\/))|(\\burl\\()([\'"])([^\\s]+?)\\3(\\))#si', // Minify HEX color code '#(?<=[\\s=:,\\(]\\#)([a-f0-6]+)\\1([a-f0-6]+)\\2([a-f0-6]+)\\3#i', // Replace `(border|outline):none` with `(border|outline):0` '#(?<=[\\{;])(border|outline):none(?=[;\\}\\!])#', // Remove empty selector(s) '#(\\/\\*(?>.*?\\*\\/))|(^|[\\{\\}])(?:[^\\s\\{\\}]+)\\{\\}#s', '#\\x1A#', ), array( '$1', '$1$2$3$4$5$6$7', '$1', ':0', '$1:0 0', '.$1', '$1$3', '$1$2$4$5', '$1$2$3', '$1:0', '$1$2', ' ' ), $input ); } /** * Compare WooCommerce function * * @param $version * @param $op * * @return bool */ public static function compareWcVersion( $version, $op ) { if ( function_exists( 'WC' ) && version_compare( WC()->version, $version, $op ) ) { return true; } return false; } /** * Check if is settings page * @return bool */ public static function isSettingsPage() { if ( is_admin() && !empty( $_GET['page'] ) && $_GET['page'] === 'dgwt_wcas_settings' ) { return true; } return false; } /** * Check if is debug page * @return bool */ public static function isDebugPage() { if ( is_admin() && !empty( $_GET['page'] ) && $_GET['page'] === 'dgwt_wcas_debug' ) { return true; } return false; } /** * Check if is Freemius checkout page * @return bool */ public static function isCheckoutPage() { if ( is_admin() && !empty( $_GET['page'] ) && $_GET['page'] === 'dgwt_wcas_settings-pricing' ) { return true; } return false; } /** * Get settings URL * * @return string */ public static function getSettingsUrl() { return admin_url( 'admin.php?page=dgwt_wcas_settings' ); } /** * Get total products * * @return int */ public static function getTotalProducts() { global $wpdb; $sql = "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish'"; $total = $wpdb->get_var( $sql ); return absint( $total ); } /** * Get all products IDs * @return array */ public static function getProductsForIndex() { global $wpdb; $sql = "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish' ORDER BY ID ASC"; $ids = $wpdb->get_col( $sql ); if ( !is_array( $ids ) || empty( $ids[0] ) || !is_numeric( $ids[0] ) ) { $ids = array(); } return $ids; } /** * Get readable format of memory * * @param int $bytes * * @return string */ public static function getReadableMemorySize( $bytes ) { $unit = array( 'b', 'kb', 'mb', 'gb', 'tb', 'pb' ); return @round( $bytes / pow( 1024, $i = floor( log( $bytes, 1024 ) ) ), 2 ) . ' ' . $unit[$i]; } /** * Get pro icon/label * * @param string $label * @param string $type * @param string $headerSubtitle * * @return string */ public static function getSettingsProLabel( $label, $type = 'header', $headerSubtitle = '' ) { $html = ''; switch ( $type ) { case 'header': if ( !empty( $headerSubtitle ) ) { $label = '' . $label . '' . $headerSubtitle . ''; } $html .= '
' . $label . '' . __( 'Pro', 'ajax-search-for-woocommerce' ) . '
'; break; case 'option-label': $html .= '
' . $label . '' . __( 'Pro', 'ajax-search-for-woocommerce' ) . '
'; break; } return $html; } /** * Calc score for searched. * * @param string $phrase Search phrase (user input). * @param string $haystack eg. product title, SKU, attribute name etc. * @param array $args Args that can wide or narrow comparison scope or change the score weight. * * @return float */ public static function calcScore( string $phrase, string $haystack, array $args = array() ) : float { $score = 0; if ( empty( $phrase ) || empty( $haystack ) ) { return $score; } // Don't apply score for a search phrase with a single character if ( strlen( $phrase ) <= 1 ) { return $score; } $default = array( 'check_similarity' => true, 'check_position' => true, 'score_containing' => 50, ); $args = array_merge( $default, $args ); $phrase = self::normalizePhrase( $phrase ); $haystack = self::normalizePhrase( $haystack ); /* -------------------------------------- * * Bonus for comparing the entire phrase * * -------------------------------------- */ $score += self::allocateScore( self::stringComparisonResult( $phrase, $haystack, $args ) ); /* ------------------------------------ * * Bonus for comparing individual words * * ------------------------------------ */ $words = explode( ' ', $phrase ); if ( count( $words ) > 1 ) { $args['check_similarity'] = false; foreach ( $words as $word ) { $score += self::allocateScore( self::stringComparisonResult( $word, $haystack, $args ) ) / 3; } } return $score; } /** * Removes multiple whitespaces, * strips whitespace (or other characters) from the beginning and end of a string * and makes a string lowercase. * * @param string $phrase The phrase to normalize. * * @return string */ public static function normalizePhrase( string $phrase ) : string { return mb_strtolower( trim( preg_replace( array('/\\s{2,}/', '/[\\t\\n]/'), ' ', $phrase ) ) ); } /** * Compare two strings and set data necessary to calculate score. * * @param string $haystack The string to search in. * @param string $needle The string need to be found. * @param array $args Args that can wide or narrow comparison scope or change the score weight. * * @return array */ public static function stringComparisonResult( string $needle = '', string $haystack = '', array $args = array() ) : array { $results = array( 'exact_match' => false, 'partial_exact_match' => false, 'containing' => false, 'containing_pos' => 0, 'text_similarity' => 0, ); $default = array( 'check_similarity' => true, 'check_position' => true, 'score_containing' => 50, ); $args = array_merge( $default, $args ); $pos = strpos( $haystack, $needle ); if ( $pos !== false ) { $results['containing'] = true; if ( $haystack === $needle ) { $results['exact_match'] = true; } elseif ( strpos( $haystack, ' ' ) !== false ) { $needleRegex = self::escPhraseForRegex( $needle ); if ( preg_match( '/\\b' . $needleRegex . '\\b/i', $haystack ) ) { $results['partial_exact_match'] = true; } if ( $args['check_position'] ) { $results['containing_pos'] = self::stringPosition( $pos, $haystack ); } } } if ( $args['check_similarity'] && strlen( $needle ) > 1 ) { $m = similar_text( $needle, $haystack, $percent ); $results['text_similarity'] = $percent; } return $results; } /** * Allocate score. Take resutls of the comarison and calculate the final score. * * @param array $comparison Data after comparing two string. * @param array $args Values and weights that are required to calculate score. * * @return float */ public static function allocateScore( array $comparison, array $args = array() ) : float { $score = 0; $default = [ 'containing_score' => 50, 'exact_match_multiplier' => 5, 'partial_exact_match_multiplier' => 2, 'text_similarity_divisor' => 3, 'containing_position_divisor' => 2, ]; $args = apply_filters( 'dgwt/wcas/score_weights', array_merge( $default, $args ) ); if ( $comparison['text_similarity'] > 0 ) { $score = $comparison['text_similarity'] / $args['text_similarity_divisor']; } // Add score based on substring position. if ( $comparison['containing'] ) { $score += $args['containing_score']; // Bonus for contained substring. // Bonus for exact match of the phrase to the text. if ( $comparison['exact_match'] ) { $score += $args['containing_score'] * $args['exact_match_multiplier']; } // Bonus for exact match of the phrase to the part of text. if ( $comparison['partial_exact_match'] ) { $score += $args['containing_score'] * $args['partial_exact_match_multiplier']; } // Bonus for substring position. if ( $comparison['containing_pos'] > 0 ) { $score += $comparison['containing_pos'] / $args['containing_position_divisor']; } } return $score; } /** * Check position of the substring relative to the whole string. * * @param int $position The result of the substr function. * @param string $haystack The string to search in. * * @return float */ public static function stringPosition( int $position, string $haystack ) : float { return 100 - $position * 100 / strlen( $haystack ); } /** * Sorting by score * * @param $a * @param $b * * @return int */ public static function cmpSimilarity( $a, $b ) { $scoreA = 0; $scoreB = 0; if ( is_object( $a ) ) { $scoreA = $a->score; $scoreB = $b->score; } if ( is_array( $a ) ) { $scoreA = $a['score']; $scoreB = $b['score']; } if ( $scoreA == $scoreB ) { return 0; } return ( $scoreA < $scoreB ? 1 : -1 ); } /** * Sorting by search resutls groups priority * * @param $a * @param $b * * @return int */ public static function sortAjaxResutlsGroups( $a, $b ) { if ( $a['order'] == $b['order'] ) { return 0; } return ( $a['order'] < $b['order'] ? -1 : 1 ); } /** * Sort from the longest to the shortest * * @param $a * @param $b * * @return int */ public static function sortFromLongest( $a, $b ) { $la = mb_strlen( $a ); $lb = mb_strlen( $b ); if ( $la == $lb ) { return strcmp( $b, $a ); } return $lb - $la; } /** * Get taxonomy parents * * @param int $term_id * @param string $taxonomy * * @return string */ public static function getTermBreadcrumbs( $termID, $taxonomy, $visited = array(), $lang = '', $exclude = array() ) { $chain = ''; $separator = ' > '; if ( Multilingual::isMultilingual() ) { $parent = Multilingual::getTerm( $termID, $taxonomy, $lang ); } else { $parent = get_term( $termID, $taxonomy ); } if ( empty( $parent ) || !isset( $parent->name ) ) { return ''; } $name = $parent->name; if ( $parent->parent && $parent->parent != $parent->term_id && !in_array( $parent->parent, $visited ) ) { $visited[] = $parent->parent; $chain .= self::getTermBreadcrumbs( $parent->parent, $taxonomy, $visited, $lang ); } if ( !in_array( $parent->term_id, $exclude ) ) { $chain .= $name . $separator; } return $chain; } /** * Get taxonomies of products attributes * * @return array * */ public static function getAttributesTaxonomies() { $taxonomies = array(); $attributeTaxonomies = wc_get_attribute_taxonomies(); if ( !empty( $attributeTaxonomies ) ) { foreach ( $attributeTaxonomies as $taxonomy ) { $taxonomies[] = 'pa_' . $taxonomy->attribute_name; } } return apply_filters( 'dgwt/wcas/attribute_taxonomies', $taxonomies ); } /** * */ public static function canInstallPremium() { } /** * Get indexer demo HTML * * @return string */ public static function indexerDemoHtml() { $html = ''; ob_start(); include DGWT_WCAS_DIR . 'partials/admin/indexer-header-demo.php'; $html .= ob_get_clean(); return $html; } /** * Get features HTML * * @return string */ public static function featuresHtml() { $html = ''; ob_start(); include DGWT_WCAS_DIR . 'partials/admin/features.php'; $html .= ob_get_clean(); return $html; } /** * Get searchable custom fields keys * * @return array */ public static function getSearchableCustomFields() { global $wpdb; $customFields = array(); $excludedMetaKeys = array( '_sku', '_wp_old_date', '_tax_status', '_stock_status', '_product_version', '_smooth_slider_style', 'auctioninc_calc_method', 'auctioninc_pack_method', '_thumbnail_id', '_product_image_gallery', 'pdf_download', 'slide_template', 'cad_iframe', 'downloads', 'edrawings_file', '3d_pdf_download', '3d_pdf_render', '_original_id' ); $excludedMetaKeys = apply_filters( 'dgwt/wcas/indexer/excluded_meta_keys', $excludedMetaKeys ); $sql = "SELECT DISTINCT meta_key\n FROM {$wpdb->postmeta} as pm\n INNER JOIN {$wpdb->posts} as p ON p.ID = pm.post_id\n WHERE p.post_type = 'product'\n AND pm.meta_value NOT LIKE 'field_%'\n AND pm.meta_value NOT LIKE 'a:%'\n AND pm.meta_value NOT LIKE '%\\%\\%%'\n AND pm.meta_value NOT LIKE '_oembed_%'\n AND pm.meta_value NOT REGEXP '^1[0-9]{9}'\n AND pm.meta_value NOT IN ('1','0','-1','no','yes','[]', '')\n "; $metaKeys = $wpdb->get_col( $sql ); if ( !empty( $metaKeys ) ) { foreach ( $metaKeys as $metaKey ) { if ( !in_array( $metaKey, $excludedMetaKeys ) && self::keyIsValid( $metaKey ) ) { $label = $metaKey; //@TODO Recognize labels based on meta key or public known as Yoast SEO etc. $customFields[] = array( 'label' => $label, 'key' => $label, ); } } } $customFields = array_reverse( $customFields ); return apply_filters( 'dgwt/wcas/indexer/searchable_custom_fields', $customFields ); } /** * Check if key is valid * * @param $key * * @return bool */ public static function keyIsValid( $key ) { return !preg_match( '/[^\\p{L}\\p{N}\\:\\.\\_\\s\\-]+/u', $key ); } /** * Check if table exist * * @return bool */ public static function isTableExists( $tableName ) { global $wpdb; $exist = false; $wpdb->hide_errors(); if ( empty( $tableName ) ) { return false; } $sql = $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'dgwt_wcas_%' ); $result = $wpdb->get_col( $sql ); if ( is_array( $result ) && in_array( $tableName, $result ) ) { $exist = true; } return $exist; } /** * Check if the engine can search in variable products * * @return bool */ public static function canSearchInVariableProducts() { global $wpdb; $allow = false; $el = $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' LIMIT 1" ); if ( !empty( $el ) && is_numeric( $el ) ) { $allow = true; } if ( DGWT_WCAS()->settings->getOption( 'search_in_product_content' ) !== 'on' ) { $allow = false; } return apply_filters( 'dgwt/wcas/search_in_variable_products', $allow ); } /** * Allow to remove method for an hook when, it's a class method used and class don't have variable, but you know the class name * * @link https://github.com/herewithme/wp-filters-extras * @return bool */ public static function removeFiltersForAnonymousClass( $hook_name = '', $class_name = '', $method_name = '', $priority = 0 ) { global $wp_filter; // Take only filters on right hook name and priority if ( !isset( $wp_filter[$hook_name][$priority] ) || !is_array( $wp_filter[$hook_name][$priority] ) ) { return false; } // Loop on filters registered foreach ( (array) $wp_filter[$hook_name][$priority] as $unique_id => $filter_array ) { // Test if filter is an array ! (always for class/method) if ( isset( $filter_array['function'] ) && is_array( $filter_array['function'] ) ) { // Test if object is a class, class and method is equal to param ! if ( is_object( $filter_array['function'][0] ) && get_class( $filter_array['function'][0] ) && get_class( $filter_array['function'][0] ) == $class_name && $filter_array['function'][1] == $method_name ) { // Test for WordPress >= 4.7 WP_Hook class (https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/) if ( is_a( $wp_filter[$hook_name], 'WP_Hook' ) ) { unset($wp_filter[$hook_name]->callbacks[$priority][$unique_id]); } else { unset($wp_filter[$hook_name][$priority][$unique_id]); } } } } return false; } /** * Create tooltip * * @param string $id * @param string $content * @param string $template * @param string $placement * @param string $class * * @return string */ public static function createTooltip( $id, $content = '', $template = '', $placement = 'right', $class = '' ) { if ( !empty( $template ) ) { $file = DGWT_WCAS_DIR . 'partials/admin/tooltips/' . $template . '.php'; if ( file_exists( $file ) ) { ob_start(); require $file; $content = ob_get_contents(); ob_end_clean(); } } $id = 'js-dgwt-wcas-tooltip-id' . sanitize_key( $id ); $html = '
'; $html .= ''; return $html; } /** * Create HTML override option tooltip * * @param string $id * @param string $content * @param string $template * @param string $placement * * @return string */ public static function getOverrideOptionText( $theme ) { $linkToShortcodesDoc = 'https://fibosearch.com/documentation/get-started/how-to-add-fibosearch-to-your-website/#add-fibosearch-with-a-shortcode'; $content = '

' . sprintf( __( 'This option is overridden by the seamless integration with the %s theme. If you want to change the value of this option, disable the integration in
WooCommerce -> FiboSearch -> Starting (tab).', 'ajax-search-for-woocommerce' ), $theme ) . '

'; $content .= '

' . sprintf( __( 'Furthermore, you can override this option for a specific search bar via shortcode params. Learn more about shortcodes parameters.', 'ajax-search-for-woocommerce' ), $linkToShortcodesDoc ) . '

'; return $content; } /** * Create HTML question mark with tooltip * * @param string $id * @param string $content * @param string $template * @param string $placement * * @return string */ public static function createQuestionMark( $id, $content = '', $template = '', $placement = 'right' ) { return self::createTooltip( $id, $content, $template, $placement, 'dashicons dashicons-editor-help dgwt-wcas-questio-mark' ); } /** * Create HTML option override tooltip * * @param string $id * @param string $content * @param string $template * @param string $placement * * @return string */ public static function createOverrideTooltip( $id, $content = '', $template = '', $placement = 'right' ) { return self::createTooltip( $id, $content, $template, $placement, 'dashicons dashicons-lock dgwt-wcas-override-tooltip' ); } /** * Get list of 24 hours */ public static function getHours() { $hours = array(); $cycle12 = ( get_option( 'time_format' ) === 'H:i' ? false : true ); for ($i = 0; $i < 24; $i++) { $label = ( $cycle12 ? $i . ':00 am' : $i . ':00' ); if ( $cycle12 && $i === 0 ) { $label = 12 . ':00 am'; } if ( $cycle12 && $i > 11 ) { if ( $i === 12 ) { $label = 12 . ':00 pm'; } else { $label = $i - 12 . ':00 pm'; } } $hours[$i] = $label; } return $hours; } /** * Get local date including timezone * * @param $timestamp * @param string $format * * @return string * @throws \Exception */ public static function localDate( $timestamp, $format = '' ) { if ( empty( $timestamp ) ) { return ''; } if ( empty( $format ) ) { $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); } $date = new \WC_DateTime("@{$timestamp}"); $date->setTimezone( new \DateTimeZone(wc_timezone_string()) ); return $date->date_i18n( $format ); } /** * Get labels * * @return array */ public static function getLabels() { $noResults = DGWT_WCAS()->settings->getOption( 'search_no_results_text', __( 'No results', 'ajax-search-for-woocommerce' ) ); $noResults = json_encode( Helpers::ksesNoResults( $noResults ), JSON_UNESCAPED_SLASHES ); $showMore = esc_html( DGWT_WCAS()->settings->getOption( 'search_see_all_results_text', __( 'See all products...', 'ajax-search-for-woocommerce' ) ) ); return apply_filters( 'dgwt/wcas/labels', array( 'post' => __( 'Post' ), 'page' => __( 'Page' ), 'vendor' => __( 'Vendor', 'ajax-search-for-woocommerce' ), 'product_plu' => __( 'Products', 'woocommerce' ), 'post_plu' => __( 'Posts' ), 'page_plu' => __( 'Pages' ), 'vendor_plu' => __( 'Vendors', 'ajax-search-for-woocommerce' ), 'sku_label' => __( 'SKU', 'woocommerce' ) . ':', 'sale_badge' => __( 'Sale', 'woocommerce' ), 'vendor_sold_by' => __( 'Sold by:', 'ajax-search-for-woocommerce' ), 'featured_badge' => __( 'Featured', 'woocommerce' ), 'in' => _x( 'in', 'in categories fe. in Books > Crime stories', 'ajax-search-for-woocommerce' ), 'read_more' => __( 'continue reading', 'ajax-search-for-woocommerce' ), 'no_results' => $noResults, 'no_results_default' => __( 'No results', 'ajax-search-for-woocommerce' ), 'show_more' => $showMore, 'show_more_details' => $showMore, 'search_placeholder' => DGWT_WCAS()->settings->getOption( 'search_placeholder', __( 'Search for products...', 'ajax-search-for-woocommerce' ) ), 'submit' => DGWT_WCAS()->settings->getOption( 'search_submit_text', '' ), 'search_hist' => __( 'Your search history', 'ajax-search-for-woocommerce' ), 'search_hist_clear' => __( 'Clear', 'ajax-search-for-woocommerce' ), ) ); } /** * Get labels * * @param string $key * * @return string */ public static function getLabel( $key ) { $label = ''; $labels = self::getLabels(); if ( array_key_exists( $key, $labels ) ) { $label = $labels[$key]; } return $label; } /** * Remove all HTML tags including