ip?. * @param \WP_Post $post Current post. * @param object $postitem Post data to link (ID, post_title, post_type). * @param string $text Processing text. */ if ( apply_filters( 'smartcrawl_autolinks_skip_post_linking', false, $post, $postitem, $text ) ) { continue; } $name = preg_quote( $postitem->post_title, '/' ); $regex = str_replace( 'KEYWORD', $name, $this->get_post_regex() ); if ( $prevent_duplicate ) { $max_single = 1; } elseif ( ! empty( $max_links ) ) { $max_single = ( $links + $max_single >= $max_links ) ? $max_links - $links : $max_single; } $arguments = array( 'target' => $this->is_option_enabled( 'target_blank' ) ? '_blank' : '', 'rel' => $this->is_option_enabled( 'rel_nofollow' ) ? 'nofollow' : '', ); $replace = '$1$2$3'; if ( $this->is_autolink_on_fly_empty( $text, $regex ) ) { continue; } // Backup previously linked text. $text = $this->backup_fragments( $text ); $newtext = preg_replace( $regex, $replace, $text, $max_single ); if ( $newtext !== $text ) { $url = get_permalink( $postitem->ID ); if ( ! $max_single_url || $urls[ $url ] < $max_single_url ) { $replacement_count = count( preg_split( $regex, $text ) ) - 1; $replacement_count = $replacement_count > 0 ? $replacement_count : 1; $links += min( $replacement_count, $max_single ); $text = str_replace( '$$$url$$$', $url, $newtext ); if ( ! isset( $urls[ $url ] ) ) { $urls[ $url ] = 1; } else { ++$urls[ $url ]; } } } } } } return $text; } /** * Insert post links to the content. * * If any of the post titles matching the words on current post content, * add links to them. * * @since 3.3.0 * * @param string $text Text to process. * @param array $urls URLs. * @param int $links Links. * @param int $max_links Max links. * @param int $max_single Max single. * @param string $max_single_url Max single URL. * @param array $ignored_array Ignored items. * * @return string */ private function link_taxonomies( $text, &$urls, &$links, $max_links, $max_single, $max_single_url, $ignored_array ) { // Get available terms for the taxonomy. $terms = $this->get_terms(); $prevent_duplicate = $this->is_option_enabled( 'customkey_preventduplicatelink' ); if ( ! empty( $terms ) ) { foreach ( $terms as $term ) { if ( ( ! $max_links || ( $links < $max_links ) ) && ! in_array( $this->is_option_enabled( 'casesens' ) ? $term->name : strtolower( $term->name ), $ignored_array, true ) ) { if ( false === $this->strpos( $text, $term->name ) ) { continue; } /** * Filters hook to short circuit the term linking. * * @since 3.3.0 * * @param bool $skip Should skip?. * @param object $term Term data to link (term_id, name, taxonomy). * @param string $text Processing text. */ if ( apply_filters( 'smartcrawl_autolinks_skip_term_linking', false, $term, $text ) ) { continue; } $name = preg_quote( $term->name, '/' ); $regexp = str_replace( 'KEYWORD', $name, $this->get_post_regex() ); $arguments = array( 'target' => $this->is_option_enabled( 'target_blank' ) ? '_blank' : '', 'rel' => $this->is_option_enabled( 'rel_nofollow' ) ? 'nofollow' : '', ); $replace = '$1$2$3'; if ( $this->is_autolink_on_fly_empty( $text, $regexp ) ) { continue; } // To prevent duplicate. $max_single = $prevent_duplicate ? 1 : $max_single; // Backup previously linked text. $text = $this->backup_fragments( $text ); $new_text = preg_replace( $regexp, $replace, $text, $max_single ); if ( $new_text !== $text ) { $url = get_term_link( get_term( $term->term_id, $term->taxonomy ) ); if ( is_wp_error( $url ) ) { continue; } if ( ! $max_single_url || $urls[ $url ] < $max_single_url ) { $replacement_count = count( preg_split( $regexp, $text ) ) - 1; $replacement_count = $replacement_count > 0 ? $replacement_count : 1; $links += min( $replacement_count, $max_single ); $text = str_replace( '$$$url$$$', $url, $new_text ); if ( ! isset( $urls[ $url ] ) ) { $urls[ $url ] = 1; } else { ++$urls[ $url ]; } } } } } } return $text; } /** * Gets all available posts on the site. * * If the posts are not found in cache, we will run a large query * to get the list. As of now only 2000 items are retrieved for performance. * * @since 3.3.0 * * @return array */ private function get_posts() { // Get post items cache. $posts = $this->cache->get_cache( 'posts', 'wds-autolinks' ); if ( ! $posts ) { global $wpdb; // Get character limit for CPT. $cpt_char_limit = $this->is_option_enabled( 'cpt_char_limit' ) ? $this->get_option( 'cpt_char_limit' ) : false; // Fallback to default. $cpt_char_limit = (int) $cpt_char_limit ? (int) $cpt_char_limit : SMARTCRAWL_AUTOLINKS_DEFAULT_CHAR_LIMIT; // Enabled post types. $enabled = $this->get_enabled_post_types(); // No need to continue if no post types are enabled. if ( empty( $enabled ) ) { return array(); } // Setup query. $query = $wpdb->prepare( "SELECT post_title, ID, post_type FROM $wpdb->posts WHERE post_status = 'publish' AND post_type IN (%s) AND LENGTH(post_title) >= %d", implode( "','", $enabled ), $cpt_char_limit ); // If no-index posts are excluded. if ( $this->is_option_enabled( 'exclude_no_index' ) ) { $query .= " AND ID NOT IN( SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wds_meta-robots-noindex' AND meta_value = '1')"; } $query .= $wpdb->prepare( ' ORDER BY LENGTH(post_title) DESC LIMIT %d', $this->get_query_limit() ); // Remove unwanted slashes to avoid query error. $query = stripslashes( $query ); $posts = $wpdb->get_results( $query ); // phpcs:ignore // Set to cache. if ( ! empty( $posts ) ) { $this->cache->set_cache( 'posts', $posts, 'wds-autolinks' ); } } /** * Filters hook to modify posts loop. * * @since 3.3.0 * * @param array $posts Posts. */ return apply_filters( 'smartcrawl_autolinks_get_posts', $posts ); } /** * Gets all available terms for enabled taxonomies. * * @since 3.3.0 * * @return array */ private function get_terms() { global $wpdb; // Check cache first. $terms = $this->cache->get_cache( 'terms', 'wds-autolinks' ); if ( ! $terms ) { $min_usage = $this->is_option_enabled( 'minusage' ) ? $this->get_option( 'minusage' ) : 1; $tax_char_limit = $this->is_option_enabled( 'tax_char_limit' ) ? $this->get_option( 'tax_char_limit' ) : false; $tax_char_limit = (int) $tax_char_limit ? (int) $tax_char_limit : \SMARTCRAWL_AUTOLINKS_DEFAULT_CHAR_LIMIT; $minimum_count = $this->is_option_enabled( 'allow_empty_tax' ) ? 0 : $min_usage; // Enabled post types. $enabled = $this->get_enabled_taxonomies(); // No need to continue if no taxonomies are enabled. if ( empty( $enabled ) ) { return array(); } // Build custom query. $query = $wpdb->prepare( "SELECT $wpdb->terms.name, $wpdb->terms.term_id, $wpdb->term_taxonomy.taxonomy FROM $wpdb->terms LEFT JOIN $wpdb->term_taxonomy " . "ON $wpdb->terms.term_id = $wpdb->term_taxonomy.term_id " . "WHERE $wpdb->term_taxonomy.taxonomy IN (%s) " . "AND LENGTH($wpdb->terms.name) >= %d " . "AND $wpdb->term_taxonomy.count >= %d " . "ORDER BY LENGTH($wpdb->terms.name) DESC LIMIT %d", implode( "','", $enabled ), $tax_char_limit, $minimum_count, $this->get_query_limit() ); // Remove unwanted slashes to avoid query error. $query = stripslashes( $query ); $terms = $wpdb->get_results( $query ); // phpcs:ignore // Set to cache. if ( ! empty( $terms ) ) { $this->cache->set_cache( 'terms', $terms, 'wds-autolinks' ); } } /** * Filters hook to modify terms list. * * @since 3.3.0 * * @param array $terms Terms. */ return apply_filters( 'smartcrawl_autolinks_get_terms', $terms ); } /** * Gets all enabled post types to link to. * * Only public post types will be selected. * * @since 3.3.0 * * @return array */ private function get_enabled_post_types() { $enabled = array(); // Get all available public post types. $post_types = get_post_types( array( 'public' => true ) ); foreach ( $post_types as $post_type ) { // Include only if enabled in settings. if ( $this->is_type_enabled( $post_type, 'link_to' ) ) { // Include to enabled types. $enabled[] = $post_type; } } /** * Filters to modify the list of enabled post types. * * @since 3.3.0 * * @param array $enabled Enabled post types. */ return apply_filters( 'smartcrawl_autolinks_get_enabled_post_types', $enabled ); } /** * Gets all enabled post types to link to. * * Only public post types will be selected. * * @since 3.3.0 * * @return array */ private function get_enabled_taxonomies() { $enabled = array(); // Get all available public post types. $taxonomies = get_taxonomies( array( 'public' => true ), 'object' ); foreach ( $taxonomies as $taxonomy ) { // Few types that we don't need. if ( in_array( $taxonomy->name, array( 'nav_menu', 'link_category', 'post_format' ), true ) ) { continue; } $key = strtolower( $taxonomy->labels->name ); // Include only if enabled in settings. if ( $this->is_type_enabled( $key, 'link_to' ) ) { // Include to enabled taxonomies. $enabled[] = $taxonomy->name; } } /** * Filters to modify the list of enabled taxonomies. * * @since 3.3.0 * * @param array $enabled Enabled taxonomies. */ return apply_filters( 'smartcrawl_autolinks_get_enabled_taxonomies', $enabled ); } /** * Checks if all requirements passed before processing. * * Checks if: * - If current page is RSS feed, check if it's enabled. * - If current page is not a single page, check if it's enabled. * - If current page is one of the ignored post skip. * * @since 3.3.0 * * @param string $type Type (post or comment). * * @return bool */ private function pre_check_passed( $type = 'post' ) { // Allow on RSS feed only if enabled. if ( is_feed() && ! $this->is_option_enabled( 'allowfeed' ) ) { return false; } // Get if post is excluded explicitly. $excluded = \smartcrawl_get_value( 'autolinks-exclude' ); if ( ! empty( $excluded ) ) { return false; } if ( 'post' === $type ) { // If only for single items, do not process archives. if ( $this->is_option_enabled( 'onlysingle' ) && ! ( is_single() || is_page() ) ) { return false; } // Verify if one of the ignored posts. $ignored_posts = \smartcrawl_get_array_value( $this->options, 'ignorepost' ); $ignored_posts = $this->explode_trim( ',', $ignored_posts ); // Get only post ids. $ignored_post_ids = array_filter( $ignored_posts, function ( $id ) { return is_numeric( $id ); } ); // If any of the ignored post. if ( ! empty( $ignored_post_ids ) && ( is_page( $ignored_post_ids ) || is_single( $ignored_post_ids ) ) ) { return false; } // Get ignored URLs. $ignored_urls = array_diff( $ignored_posts, $ignored_post_ids ); if ( empty( $ignored_urls ) ) { return true; } // Get relative url of the current page. // @todo Improve below code. We are using too many strip functions. $relative_permalink = untrailingslashit( str_replace( untrailingslashit( home_url() ), '', get_the_permalink() ) ); foreach ( $ignored_urls as $ignored_url ) { $ignored_url = untrailingslashit( $ignored_url ); // Should start with a slash. if ( strpos( $ignored_url, '/' ) === 0 ) { if ( // If wildcard URL. substr( $ignored_url, - 2, 2 ) === '/*' && // If starting with ignored url. strpos( $relative_permalink, rtrim( $ignored_url, '/*' ) ) === 0 ) { return false; } elseif ( $ignored_url === $relative_permalink ) { // If matching ignored url. return false; } } } } return true; } /** * Backup fragments. * * @param string $text Text. * * @return array|string|string[]|null */ private function backup_fragments( $text ) { $utf = $this->is_utf8_matching_enabled() ? 'u' : ''; $tags = 'a|script|style'; // Exclude headings. if ( $this->is_option_enabled( 'excludeheading' ) ) { $tags .= '|h1|h2|h3|h4|h5|h6'; } // Exclude image captions. if ( $this->is_option_enabled( 'exclude_image_captions' ) ) { $tags .= '|figcaption'; } return preg_replace_callback( '/<\s*(' . $tags . ')[^>]*>.*?<\s*\/\1\s*>/is' . $utf, array( $this, 'replace_fragment_with_placeholder' ), $text ); } /** * Restore backed up fragments. * * @since 1.0.0 * * @param string $text Text. * * @return array|string|string[] */ private function restore_fragments( $text ) { return str_replace( array_keys( $this->fragments ), array_values( $this->fragments ), $text ); } /** * Replace fragment with placeholder. * * These placeholders are used to restore the backups. * * @since 1.0.0 * * @param array $matches Matches. * * @return string */ private function replace_fragment_with_placeholder( $matches ) { $fragment = $matches[0]; $fragment_hash = md5( $fragment ); $placeholder = ""; $this->fragments[ $placeholder ] = $fragment; return $placeholder; } /** * Is UTF matching enabled?. * * @since 1.0.0 * * @return bool */ private function is_utf8_matching_enabled() { $utf8_variations = array( 'utf8', 'utf-8', 'UTF8', 'UTF-8' ); $is_utf8_site = ( ( ! defined( '\DB_CHARSET' ) || strpos( \DB_CHARSET, 'utf8' ) !== false ) && in_array( get_option( 'blog_charset', '' ), $utf8_variations, true ) ); /** * Filters hook to modify utf8 matching check. * * @since 1.0.0 * * @param bool $is_utf8_site Is enabled. */ return apply_filters( 'smartcrawl_autolinks_utf8_matching_enabled', $is_utf8_site ); } /** * Gets a single post cache data. * * @since 3.3.0 * * @param int $id Item ID. * @param string $type Type (post or comment). * * @return mixed */ private function get_item_cache( $id, $type = 'post' ) { return $this->cache->get_cache( 'content-' . $id, 'wds-autolinks-' . $type ); } /** * Sets a single post cache. * * @since 3.3.0 * * @param int $id Post ID. * @param mixed $data Content to store. * @param string $type Type (post or comment). * * @return void */ private function set_item_cache( $id, $data, $type = 'post' ) { $this->cache->set_cache( 'content-' . $id, $data, 'wds-autolinks-' . $type ); } /** * Delete a single post cache. * * @since 3.3.0 * * @param int $id Post ID. * @param string $type Type (post or comment). * * @return void */ private function delete_item_cache( $id, $type = 'post' ) { $this->cache->purge_cache( 'content-' . $id, 'wds-autolinks-' . $type ); } /** * Checks if an option is enabled. * * @since 3.3.0 * * @param string $key Setting key. * * @return bool */ private function is_option_enabled( $key ) { $option = $this->get_option( $key ); return ! empty( $option ); } /** * Checks if a type is enabled. * * @since 3.4.3 * * @param string $type Type to check. * @param string $key Setting key. * * @return bool */ private function is_type_enabled( $type, $key = 'insert' ) { $option = (array) $this->get_option( $key, array() ); $type = 'insert' === $key ? $type : "l$type"; return in_array( $type, $option, true ); } /** * Checks if auto linking on the fly is enabled and regex matches. * * @since 3.3.0 * * @param string $text Text to process. * @param string $regex Regular expression. * * @return bool */ private function is_autolink_on_fly_empty( $text, $regex ) { return ( ( defined( '\SMARTCRAWL_AUTOLINKS_ON_THE_FLY_CHECK' ) && \SMARTCRAWL_AUTOLINKS_ON_THE_FLY_CHECK ) && ! preg_match( $regex, strip_shortcodes( $text ) ) ); } /** * Gets a single setting value. * * @param string $key Option name. * @param mixed $default_value Default value. * * @return mixed * * @since 3.3.0 */ private function get_option( $key, $default_value = false ) { return isset( $this->options[ $key ] ) ? $this->options[ $key ] : $default_value; } /** * Gets the limit for query. * * @since 3.3.0 * * @return int */ private function get_query_limit() { // If you want to increase the no. of items in query define this. return defined( 'SMARTCRAWL_AUTOLINKS_GET_POSTS_LIMIT' ) ? intval( \SMARTCRAWL_AUTOLINKS_GET_POSTS_LIMIT ) : 2000; } /** * Text to trimmed array of strings * * @since 1.0.0 * * @param string $separator Separator to break the text on. * @param string $text Text to break. * * @return array */ private function explode_trim( $separator, $text ) { $arr = empty( $text ) ? array() : explode( $separator, $text ); $ret = array(); foreach ( $arr as $e ) { $ret[] = trim( $e ); } return $ret; } /** * Gets regular expression for the content. * * @since 3.3.0 * * @return string */ private function get_post_regex() { // Don't match. $lookahead_parts = array( '[^<]+[>]+', // Name of HTML tags e.g. block in
. '[\[\]]+', // @todo see what this one does. ); $negative_lookahead = join( '|', $lookahead_parts ); $negative_lookahead = "(?!(?:$negative_lookahead))"; // Base regular expression. $regex = "/$negative_lookahead(^|\b|[^<\p{L}\/>])(KEYWORD)([^\p{L}\/>]|\b|$)/msU"; // If case insensitive. if ( ! $this->is_option_enabled( 'casesens' ) ) { $regex .= 'i'; } // Enable UTF-8 flag in the regex. if ( $this->is_utf8_matching_enabled() ) { $regex .= 'u'; } /** * Filters hook to modify post regex. * * @since 3.3.0 * * @param string $regex Regex. */ return apply_filters( 'smartcrawl_autolinks_get_post_regex', $regex ); } /** * Gets absolute URL. * * @since 1.0.0 * * @param string $url URL. * * @return string */ private function get_absolute_url( $url ) { $is_relative = strpos( $url, '/' ) === 0; return $is_relative ? trailingslashit( home_url( $url ) ) : $url; } /** * Find the position of the first occurrence of a substring in a string. * * Alias for strpos & stripos based on the case sensitive option. * * @since 3.3.0 * * @param string $haystack Text to process. * @param string $needle String to check. * * @return false|int */ private function strpos( $haystack, $needle ) { return $this->is_option_enabled( 'casesens' ) ? strpos( $haystack, $needle ) : stripos( $haystack, $needle ); } /** * Checks if content cache can be used. * * @since 3.3.2 * * @param string $type Post type. * * @return bool */ private function can_cache_content( $type = 'post' ) { // If cache is not disabled. $can_cache = ! $this->is_option_enabled( 'disable_content_cache' ); /** * Filters hook to modify caching of content. * * @since 3.3.2 * * @param bool $can_cache Can cache. * @param string $type Post type. */ return apply_filters( 'smartcrawl_autolinks_can_cache_content', $can_cache, $type ); } /** * Gets site service instance. * * @return object */ private function is_premium_member() { return Service::get( Service::SERVICE_SITE )->is_member(); } /** * Get the insert options. * * @return array */ public function get_insert_options() { $result = array(); foreach ( $this->get_insert_keys() as $key => $label ) { $result[ $key ] = array( 'label' => $label, 'value' => ! empty( $this->options[ $key ] ), ); } return $result; } /** * Get the insert keys. * * @return array */ private function get_insert_keys() { // Add post types. foreach ( \smartcrawl_get_post_types() as $post_type => $pt ) { $key = strtolower( $pt->name ); $insert[ $key ] = $pt->labels->name; } // Add comments. $insert['comment'] = __( 'Comments', 'wds' ); // Add Woo Product category. if ( taxonomy_exists( 'product_cat' ) ) { $taxonomy = get_taxonomy( 'product_cat' ); // Add product category. $insert['product_cat'] = empty( $taxonomy->label ) ? __( 'Product Categories', 'wds' ) : $taxonomy->label; } return $insert; } /** * Get link to options. * * @return array */ public function get_linkto_options() { $result = array(); foreach ( $this->get_linkto_keys() as $key => $label ) { $result[ $key ] = array( 'label' => $label, 'value' => ! empty( $this->options[ $key ] ), ); } return $result; } /** * Get link to keys. * * @return array */ private function get_linkto_keys() { $post_types = array(); foreach ( \smartcrawl_get_post_types() as $post_type => $pt ) { $key = strtolower( $pt->name ); $post_types[ 'l' . $key ] = $pt->labels->name; } $taxonomies = array(); foreach ( get_taxonomies( array( 'public' => true ) ) as $taxonomy ) { if ( ! in_array( $taxonomy, array( 'nav_menu', 'link_category', 'post_format' ), true ) ) { $tax = get_taxonomy( $taxonomy ); $key = strtolower( $tax->labels->name ); $taxonomies[ 'l' . $key ] = $tax->labels->name; } } return array_merge( $post_types, $taxonomies ); } /** * Sets submodule options. * * @param array $options Submodule options. * * @return void */ public function set_options( $options = array() ) { $this->options = $options; if ( ! is_array( $this->options ) ) { $this->options = array(); } if ( empty( $this->options['ignorepost'] ) ) { $this->options['ignorepost'] = ''; } if ( empty( $this->options['ignore'] ) ) { $this->options['ignore'] = ''; } if ( empty( $this->options['customkey'] ) ) { $this->options['customkey'] = ''; } if ( empty( $this->options['cpt_char_limit'] ) ) { $this->options['cpt_char_limit'] = ''; } if ( empty( $this->options['tax_char_limit'] ) ) { $this->options['tax_char_limit'] = ''; } if ( ! isset( $this->options['link_limit'] ) ) { $this->options['link_limit'] = ''; } if ( ! isset( $this->options['single_link_limit'] ) ) { $this->options['single_link_limit'] = ''; } } }