PluginProbe
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms / 3.44.0
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms v3.44.0
3.53.0 3.52.0 3.51.0 3.50.0 3.45.0 3.38.0 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.40.0 3.40.1 3.41.0 3.42.0 3.43.0 3.44.0 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 All 177 releases
simple-tags / inc / class.client.autolinks.php

class.client.autolinks.php in Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms 3.44.0, at inc/class.client.autolinks.php

1,196 lines 41.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class SimpleTags_Client_Autolinks
4 {
5
6 public static $posts = array();
7 public static $link_tags = array();
8 public static $tagged_link_count = 0;
9
10 /**
11 * Constructor
12 *
13 * @return void
14 * @author WebFactory Ltd
15 */
16 public function __construct()
17 {
18
19 if (1 === (int) SimpleTags_Plugin::get_option_value('active_auto_links')) {
20
21 $auto_link_priority = SimpleTags_Plugin::get_option_value('auto_link_priority');
22 if (0 === (int) $auto_link_priority) {
23 $auto_link_priority = 12;
24 }
25
26 // Auto link tags
27 add_filter('the_posts', array(__CLASS__, 'the_posts'), 10);
28
29 //new UI
30 add_filter('the_content', array(__CLASS__, 'taxopress_autolinks_the_content'), 5);
31 add_filter('the_title', array(__CLASS__, 'taxopress_autolinks_the_title'), 5);
32
33 // Elementor compatibility: elementor outputs content through its own filters,
34 // so also run our autolinks on those outputs.
35 if ( defined('ELEMENTOR_VERSION') || class_exists('\Elementor\Plugin') ) {
36 add_filter('elementor/frontend/the_content', array(__CLASS__, 'taxopress_autolinks_the_content'), 5);
37 add_filter('elementor/frontend/builder_content', array(__CLASS__, 'taxopress_autolinks_the_content'), 5);
38 }
39
40 add_action('admin_init', [$this, 'taxopress_customurl_taxonomies_fields']);
41 }
42 }
43
44 public function taxopress_customurl_taxonomies_fields() {
45
46 if (!$this->is_taxonomy_page()) {
47 return;
48 }
49
50 // Cache the autolink settings to avoid repeated database calls
51 static $enabled_taxonomies = null;
52
53 if ($enabled_taxonomies === null) {
54 $autolink_settings = taxopress_get_autolink_data();
55 $enabled_taxonomies = $this->get_enabled_taxonomies($autolink_settings);
56 }
57
58 // Only get taxonomies that are actually enabled
59 $taxonomies = get_taxonomies([], 'objects');
60
61 foreach ($enabled_taxonomies as $taxonomy_name) {
62 if (isset($taxonomies[$taxonomy_name])) {
63 add_action("{$taxonomy_name}_edit_form_fields", [$this, 'taxopress_add_custom_url_field']);
64 add_action("{$taxonomy_name}_add_form_fields", [$this, 'taxopress_add_custom_url_field_new']);
65 add_action("edited_{$taxonomy_name}", [$this, 'taxopress_save_custom_url_field']);
66 add_action("created_{$taxonomy_name}", [$this, 'taxopress_save_custom_url_field']);
67 }
68 }
69 }
70
71 private function is_taxonomy_page() {
72 global $pagenow;
73 return in_array($pagenow, ['edit-tags.php', 'term.php']);
74 }
75
76 private function get_enabled_taxonomies($autolink_settings) {
77 $default_enabled_taxonomies = ['post_tag', 'category'];
78 $enabled_taxonomies = [];
79 $has_setting_saved = false;
80
81 foreach ($autolink_settings as $setting) {
82 if (array_key_exists('enable_customurl_field', $setting)) {
83 $has_setting_saved = true;
84 $enabled_taxonomies = is_array($setting['enable_customurl_field']) ? $setting['enable_customurl_field'] : [];
85 break;
86 }
87 }
88
89 return $has_setting_saved ? array_unique($enabled_taxonomies) : $default_enabled_taxonomies;
90 }
91
92 /**
93 * Stock posts ID as soon as possible
94 * TODO: test if post_type allow post_tag before keep post ID
95 *
96 * @param array $posts
97 *
98 * @return array
99 */
100 public static function the_posts($posts)
101 {
102 if (!empty($posts) && is_array($posts)) {
103 foreach ((array) $posts as $post) {
104 self::$posts[] = (int) $post->ID;
105 }
106
107 self::$posts = array_unique(self::$posts);
108 }
109
110 return $posts;
111 }
112
113 /**
114 * Get tags from current post views
115 *
116 * @return array
117 */
118 public static function get_tags_from_current_posts($options = false)
119 {
120
121 if (is_array(self::$posts) && count(self::$posts) > 0) {
122 // Generate SQL from post id
123 $postlist = implode("', '", self::$posts);
124
125 // Generate key cache
126 $key = md5(maybe_serialize($postlist));
127
128 $results = array();
129
130 if ($options) {
131 $term_taxonomy = $options['taxonomy'];
132 } else {
133 $term_taxonomy = 'post_tag';
134 }
135
136 // Get cache if exist
137 $cache = wp_cache_get('generate_keywords', 'simple-tags');
138 if ($options || false === $cache) {
139 if ($cache === false) {
140 $cache = [];
141 }
142 foreach (self::$posts as $object_id) {
143 // Get terms
144 $terms = get_object_term_cache($object_id, $term_taxonomy);
145 if (false === $terms || is_wp_error($terms)) {
146 $terms = wp_get_object_terms($object_id, $term_taxonomy);
147 }
148
149 if (false !== $terms && !is_wp_error($terms)) {
150 $results = array_merge($results, $terms);
151 }
152 }
153
154 $cache[$key] = $results;
155 wp_cache_set('generate_keywords', $cache, 'simple-tags');
156 } else {
157 if (isset($cache[$key])) {
158 return $cache[$key];
159 }
160 }
161
162 return $results;
163 }
164
165 return array();
166 }
167
168 /**
169 * Get all available local tags
170 *
171 * @return array
172 */
173 public static function get_all_post_tags($options = false)
174 {
175 if (is_array(self::$posts) && count(self::$posts) > 0) {
176 // Generate SQL from post id
177 $postlist = implode("', '", self::$posts);
178
179 // Generate key cache
180 $key = md5(maybe_serialize($postlist));
181
182 if ($options) {
183 $term_taxonomy = $options['taxonomy'];
184 } else {
185 $term_taxonomy = 'post_tag';
186 }
187 $results = get_tags(['taxonomy' => $term_taxonomy, 'hide_empty' => false]);
188 // Get cache if exist
189 $cache = wp_cache_get('generate_keywords', 'simple-tags');
190 if ($options || false === $cache) {
191 foreach (self::$posts as $object_id) {
192 // Get terms
193 $terms = get_object_term_cache($object_id, $term_taxonomy);
194 if (false === $terms || is_wp_error($terms)) {
195 $terms = wp_get_object_terms($object_id, $term_taxonomy);
196 }
197
198 if (false !== $terms && !is_wp_error($terms)) {
199 $results = array_merge($results, $terms);
200 }
201 }
202 $cache = [];
203 $cache[$key] = $results;
204 wp_cache_set('generate_keywords', $cache, 'simple-tags');
205 } else {
206 if (isset($cache[$key])) {
207 return $cache[$key];
208 }
209 }
210
211 return $results;
212 }
213
214 return array();
215 }
216
217 public function taxopress_add_custom_url_field($term) {
218
219 if (!is_object($term)) {
220 return;
221 }
222
223 $taxopress_custom_url = get_term_meta($term->term_id, 'taxopress_custom_url', true);
224
225 ?>
226 <tr class="form-field">
227 <th scope="row" valign="top">
228 <label for="taxopress_custom_url"><?php esc_html_e('Custom URL', 'simple-tags'); ?></label>
229 </th>
230 <td>
231 <input type="text" name="taxopress_custom_url" id="taxopress_custom_url" value="<?php echo esc_attr($taxopress_custom_url); ?>" size="40">
232 <p class="description">
233 <?php esc_html_e('Enter a custom URL for this term. This URL will only be used for auto-linked terms. If left empty, the term will link to its archive page.', 'simple-tags'); ?>
234 </p>
235
236 </td>
237 </tr>
238 <?php
239 }
240
241 public function taxopress_add_custom_url_field_new() {
242 ?>
243 <div class="form-field">
244 <label for="taxopress_custom_url"><?php esc_html_e('Custom URL', 'simple-tags'); ?></label>
245 <input type="text" name="taxopress_custom_url" id="taxopress_custom_url" value="" size="40">
246 <p class="description">
247 <?php esc_html_e('Enter a custom URL for this term. This URL will only be used if the term is auto-linked. If left empty, the term will link to its archive page.', 'simple-tags'); ?>
248 </p>
249 </div>
250 <?php
251 }
252
253 public function taxopress_save_custom_url_field($term_id) {
254 if (!empty($_POST['taxopress_custom_url'])) {
255 $taxopress_custom_url = sanitize_url(wp_unslash($_POST['taxopress_custom_url']));
256 update_term_meta($term_id, 'taxopress_custom_url', $taxopress_custom_url);
257 } else {
258 delete_term_meta($term_id, 'taxopress_custom_url');
259 }
260
261 }
262
263 /**
264 * Get links for each tag for auto link feature
265 *
266 */
267 public static function prepare_auto_link_tags($options = false)
268 {
269 global $post;
270 if ($options) {
271 $auto_link_min = (int) $options['autolink_usage_min'];
272 $unattached_terms = (int) $options['unattached_terms'];
273 $autolink_min_char = (int) $options['autolink_min_char'];
274 $autolink_max_char = (int) $options['autolink_max_char'];
275 $term_taxonomy = $options['taxonomy'];
276 $custom_urls_enabled = !empty($options['enable_custom_urls']);
277 } else {
278 $auto_link_min = (int) SimpleTags_Plugin::get_option_value('auto_link_min');
279 $unattached_terms = (int) SimpleTags_Plugin::get_option_value('auto_link_all');
280 $autolink_min_char = 0;
281 $autolink_max_char = 0;
282 $term_taxonomy = 'post_tag';
283 $custom_urls_enabled = false;
284 $autolink_settings = taxopress_get_autolink_data();
285 foreach ($autolink_settings as $setting) {
286 if (!empty($setting['enable_custom_urls'])) {
287 $custom_urls_enabled = true;
288 break;
289 }
290 }
291 }
292
293 if (1 === $unattached_terms) {
294 $terms = self::get_all_post_tags($options);
295 } else {
296 $terms = self::get_tags_from_current_posts($options);
297 }
298
299 $custom_urls_enabled = !empty($options['enable_custom_urls']);
300 $archivepage = !empty($options['archivepage']);
301
302 if (!$archivepage && !$custom_urls_enabled) {
303 self::$link_tags = [];
304 return true;
305 }
306
307 $custom_urls = [];
308 if ($custom_urls_enabled && !empty($terms)) {
309 $term_ids = wp_list_pluck($terms, 'term_id');
310 $custom_urls = self::get_term_meta_batch($term_ids, 'taxopress_custom_url');
311 }
312
313 foreach ((array) $terms as $term) {
314
315 //hidden terms should not be auto linked
316 if ((int) SimpleTags_Plugin::get_option_value('enable_hidden_terms') === 1) {
317 $min_usage = (int) SimpleTags_Plugin::get_option_value('hide-rarely');
318 if ($term->count < $min_usage) {
319 continue;
320 }
321 }
322
323 if ($options && isset($options['autolink_usage_min'])) {
324 $autolink_min_usage = (int) $options['autolink_usage_min'];
325 if ($term->count < $autolink_min_usage) {
326 continue;
327 }
328 }
329
330 if (!$archivepage && $custom_urls_enabled) {
331 $taxopress_custom_url = isset($custom_urls[$term->term_id])
332 ? $custom_urls[$term->term_id]
333 : '';
334 if (!empty($taxopress_custom_url)) {
335 self::$link_tags[$term->name] = esc_url($taxopress_custom_url);
336 }
337 } else {
338 //add primary term
339 if ($custom_urls_enabled) {
340 $taxopress_custom_url = isset($custom_urls[$term->term_id])
341 ? $custom_urls[$term->term_id]
342 : '';
343 $primary_term_link = !empty($taxopress_custom_url) ? esc_url($taxopress_custom_url) : get_term_link($term, $term->taxonomy);
344 } else {
345 $primary_term_link = get_term_link($term, $term->taxonomy);
346 }
347 $add_terms = [];
348 // store the URL
349 self::$link_tags[$term->name] = $primary_term_link;
350 $add_terms[$term->name] = $primary_term_link;
351
352 // add term synonyms
353 if (is_array($options) && isset($options['synonyms_link']) && (int)$options['synonyms_link'] > 0) {
354 $term_synonyms = taxopress_get_term_synonyms($term->term_id);
355 if (!empty($term_synonyms)) {
356 foreach ($term_synonyms as $term_synonym) {
357 $add_terms[$term_synonym] = $primary_term_link;
358 }
359 }
360 }
361
362 // add linked term
363 $add_terms = taxopress_add_linked_term_options($add_terms, $term->name, $term->taxonomy, true);
364
365 foreach ($add_terms as $add_name => $add_term_link) {
366 //min character check
367 $min_char_pass = true;
368 if ($autolink_min_char > 0) {
369 $min_char_pass = strlen($add_name) >= $autolink_min_char ? true : false;
370 }
371 //max character check
372 $max_char_pass = true;
373 if ($autolink_max_char > 0) {
374 $max_char_pass = strlen($add_name) <= $autolink_max_char ? true : false;
375 }
376
377 if ($auto_link_min === 0 || $term->count >= $auto_link_min && $min_char_pass && $max_char_pass) {
378 self::$link_tags[$add_name] = esc_url($add_term_link);
379 }
380 }
381 }
382 }
383 return true;
384 }
385
386 /**
387 * Helper function to batch load term meta to reduce database queries
388 */
389 private static function get_term_meta_batch($term_ids, $meta_key) {
390 global $wpdb;
391
392 if (empty($term_ids)) {
393 return [];
394 }
395
396 // Sanitize term IDs
397 $term_ids = array_map('intval', $term_ids);
398 $term_ids_string = implode(',', $term_ids);
399
400 $results = $wpdb->get_results($wpdb->prepare(
401 "SELECT term_id, meta_value FROM {$wpdb->termmeta}
402 WHERE term_id IN ($term_ids_string) AND meta_key = %s",
403 $meta_key
404 ));
405
406 $batch_data = [];
407 foreach ($results as $result) {
408 $batch_data[$result->term_id] = $result->meta_value;
409 }
410
411 return $batch_data;
412 }
413
414 private static function taxopress_get_title_attribute($search, $url, $options) {
415
416 $title_attribute = isset($options['autolink_title_attribute'])
417 ? $options['autolink_title_attribute']
418 : SimpleTags_Plugin::get_option_value('auto_link_title');
419
420 $term = get_term_by('name', $search, $options['taxonomy']);
421 if ($term) {
422 $custom_url = get_term_meta($term->term_id, 'taxopress_custom_url', true);
423 if (!empty($custom_url) && esc_url($custom_url) === $url) {
424 $title_attribute = isset($options['autolink_title_attribute_when_using_custom_url'])
425 ? $options['autolink_title_attribute_when_using_custom_url']
426 : SimpleTags_Plugin::get_option_value('auto_link_title_custom_url');
427 }
428 }
429
430 return esc_attr(sprintf($title_attribute, $search));
431 }
432
433 /**
434 * Replace text by link, except HTML tag, and already text into link, use DOMdocument.
435 * https://stackoverflow.com/questions/4044812/regex-domdocument-match-and-replace-text-not-in-a-link
436 *
437 * @param string $content
438 * @param string $search
439 * @param string $replace
440 * @param string $case
441 * @param string $rel
442 *
443 * @return void
444 */
445 private static function replace_by_links_dom(&$content, $search = '', $replace = '', $case = '', $rel = '', $options = false, $content_type = 'content')
446 {
447 global $post, $autolinked_contents;
448
449 if (!is_object($post) || !isset($post->ID) || empty($content)) {
450 return $content;
451 }
452
453 $dom = new DOMDocument();
454
455 if (!is_array($search)) {
456 $search_lists = [];
457 $search_lists[] = [
458 'term_name' => $search,
459 'term_link' => $replace,
460 'case' => $case,
461 'rel' => $rel,
462 'options' => $options,
463 'option_id' => 0,
464 'option_idxx' => 0,
465 'post_limit' => SimpleTags_Plugin::get_option_value('auto_link_max_by_post'),
466 'term_limit' => SimpleTags_Plugin::get_option_value('auto_link_max_by_tag'),
467 'type' => $content_type,
468 ];
469 } else {
470 $search_lists = $search;
471 }
472
473 if (empty($search_lists)) {
474 return;
475 }
476
477 $content_type = $search_lists[0]['type'];
478
479 $content_key = $content_type . '_' . $post->ID;
480
481 if (!is_array($autolinked_contents)) {
482 $autolinked_contents = [];
483 }
484
485
486 if (isset($autolinked_contents[$content_key])) {
487 $content = $autolinked_contents[$content_key];
488 return $content;
489 }
490
491 // process blocks exclusion
492 if (is_array($search_lists[0]['options']) && !empty($search_lists[0]['options']['blocks_exclusion'])) {
493 $blocks_exclusion = $search_lists[0]['options']['blocks_exclusion'];
494 $process = function($item) {
495 // Replace "core/" prefixes
496 $item = str_replace("core/", "", $item);
497 // Add "wp:" prefixes
498 return "wp:" . $item;
499 };
500 // Apply the processing function to each element of the array
501 $blocks_to_exclude = array_map($process, $blocks_exclusion);
502
503 // Escape special characters for regex pattern
504 $escaped_blocks = array_map(function($block) {
505 return preg_quote($block, '/');
506 }, $blocks_to_exclude);
507
508 // Create a regex pattern from the block names
509 $pattern = '/<!-- (' . implode('|', $escaped_blocks) . ')(.*?)-->(.*?)<!-- \/wp\:(.*?) -->/s';
510
511 $content = preg_replace_callback(
512 $pattern,
513 function ($matches) {
514 // Return the matched block content wrapped in taxopressnotag
515 return '<taxopressnotag>' . $matches[0] . '</taxopressnotag>';
516 },
517 $content
518 );
519 }
520
521 // process shortcodes exclusion
522 if (is_array($search_lists[0]['options']) && !empty($search_lists[0]['options']['shortcodes_exclusion'])) {
523 $shortcodes_exclusion = $search_lists[0]['options']['shortcodes_exclusion'];
524
525 // Create a regex pattern to match any of the shortcodes with or without attributes
526 $pattern = '/\[(?:' . implode('|', $shortcodes_exclusion) . ')[^\]]*\]/';
527
528 // Define the replacement function to wrap matched shortcodes
529 $callback = function($matches) {
530 return '<taxopressnotag>' . $matches[0] . '</taxopressnotag>';
531 };
532
533 // Apply the regex pattern to the content using the callback
534 $content = preg_replace_callback($pattern, $callback, $content);
535 }
536
537 //replace html entity with their entity code
538 foreach (taxopress_html_character_and_entity() as $enity => $code) {
539 $content = str_replace($enity, $code, $content);
540 }
541
542 // Replace HTML entities with placeholders
543 $content = preg_replace_callback('/&#(\d+);/', function($matches) {
544 return 'STARTTAXOPRESSENTITY' . $matches[1] . 'TAXOPRESSENTITYEND';
545 }, $content);
546
547 //$content = str_replace('&#','|--|',$content);//https://github.com/TaxoPress/TaxoPress/issues/824
548 //$content = str_replace('&','&#38;',$content); //https://github.com/TaxoPress/TaxoPress/issues/770*/
549 $content = 'starttaxopressrandom' . $content . 'endtaxopressrandom'; //we're having issue when content start with styles https://wordpress.org/support/topic/3-7-2-auto-link-case-not-working/#post-16665257
550 //$content = utf8_decode($content);
551
552 libxml_use_internal_errors(true);
553 // loadXml needs properly formatted documents, so it's better to use loadHtml, but it needs a hack to properly handle UTF-8 encoding
554 //$result = $dom->loadHtml(mb_convert_encoding($content, 'HTML-ENTITIES', "UTF-8"));
555 //$result = $dom->loadHtml(htmlspecialchars_decode($content, ENT_QUOTES | ENT_HTML5));
556
557 // Load the content as HTML without adding DOCTYPE and html/body tags
558 $content = '<div>' . $content . '</div>';
559
560 $content = mb_encode_numericentity($content, [0x80, 0x10FFFF, 0, 0xFFFFF], 'UTF-8');
561
562 $result = $dom->loadHTML($content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
563
564 if (false === $result) {
565 return;
566 }
567
568 $xpath = new DOMXPath($dom);
569 $j = 0;
570 $replaced_count = 0;
571
572 $replaced_tags_counts = [];
573 $option_limits = [];
574 $term_limits = [];
575 $option_remaining = [];
576 $option_tagged_counts = [];
577 $node_text = [];
578
579 foreach ($search_lists as $search_details) {
580
581 $search = $search_details['term_name'];
582 $replace = $search_details['term_link'];
583 $case = $search_details['case'];
584 $rel = $search_details['rel'];
585 $options = $search_details['options'];
586
587 $search = str_replace('&amp;', 'taxopressamp', $search); // https://github.com/TaxoPress/TaxoPress/issues/1638
588
589 if (is_array($options)) {
590 $autolink_case = $options['autolink_case'];
591 $html_exclusion = $options['html_exclusion'];
592 $html_exclusion_customs = isset($options['html_exclusion_customs']) ? $options['html_exclusion_customs'] : [];
593 $exclude_class = $options['autolink_exclude_class'];
594 $title_attribute = $options['autolink_title_attribute'];
595 $title_attribute_custom_url = $options['autolink_title_attribute_when_using_custom_url'];
596 $link_class = isset($options['link_class']) ? taxopress_format_class($options['link_class']) : '';
597 } else {
598 $autolink_case = 'lowercase';
599 $html_exclusion = [];
600 $html_exclusion_customs = [];
601 $exclude_class = '';
602 $title_attribute = SimpleTags_Plugin::get_option_value('auto_link_title');
603 $title_attribute_custom_url = SimpleTags_Plugin::get_option_value('auto_link_title_custom_url');
604 $link_class = '';
605 }
606
607 $detail_id = $search_details['type'] . '_' . $search_details['option_id'];
608
609 if (!isset($option_limits[$detail_id])) {
610 $option_limits[$detail_id] = $search_details['post_limit'];
611 }
612
613 if (!isset($option_remaining[$detail_id])) {
614 $option_remaining[$detail_id] = $option_limits[$detail_id];
615 }
616
617 if (!isset($term_limits[$detail_id])) {
618 $term_limits[$detail_id] = min($search_details['term_limit'], $option_remaining[$detail_id]);
619 }
620
621 if (!isset($option_tagged_counts[$detail_id])) {
622 $option_tagged_counts[$detail_id] = 0;
623 }
624
625 $html_exclusion[] = 'taxopressnotag';
626 $html_exclusion[] = 'meta';
627 $html_exclusion[] = 'link';
628 $html_exclusion[] = 'head';
629
630 if (!empty($html_exclusion_customs)) {
631 $html_exclusion = array_merge($html_exclusion, $html_exclusion_customs);
632 }
633
634 //auto link exclusion
635 $exclusion = '[not(ancestor::a)][not(ancestor-or-self::a/@*)]';
636 if (count($html_exclusion) > 0) {
637 foreach ($html_exclusion as $exclude_ancestor) {
638 $exclusion .= '[not(ancestor::' . strtolower($exclude_ancestor) . ')][not(ancestor-or-self::' . strtolower($exclude_ancestor) . '/@*)]';
639 }
640 }
641
642 // Prepare exclude terms array
643 $excludes_class = explode(',', $exclude_class);
644 if (!empty($excludes_class)) {
645 $excludes_class = array_filter($excludes_class);
646 $excludes_class = array_unique($excludes_class);
647 if (count($excludes_class) > 0) {
648 foreach ($excludes_class as $idclass) {
649 if (substr(trim($idclass), 0, 1) === "#") {
650 $element_id = ltrim(trim($idclass), "#");
651 $exclusion .= "[not(ancestor::*[@id='$element_id'])]";
652 } else {
653 $element_class = ltrim(trim($idclass), ".");
654 $exclusion .= "[not(ancestor::*[@class='$element_class'])]";
655 }
656 }
657 }
658 }
659
660 foreach ($xpath->query('//text()' . $exclusion . '') as $node) {
661 // Exclude URLs from being replaced
662 if (preg_match('/(http|https):\/\/[^\s]+/i', $node->wholeText)) {
663 continue;
664 }
665 $url = array_key_exists($search, self::$link_tags) ? self::$link_tags[$search] : $replace;
666 $used_title = self::taxopress_get_title_attribute($search, $url, $options);
667 $replace = $url;
668 $substitute = '<a href="' . $replace . '" class="st_tag internal_tag ' . $link_class . '" ' . $rel . ' title="' . $used_title . "\">$search</a>";
669 $link_openeing = '<a href="' . $replace . '" class="st_tag internal_tag ' . $link_class . '" ' . $rel . ' title="' . $used_title . "\">";
670 $link_closing = '</a>';
671 $upperterm = strtoupper($search);
672 $lowerterm = strtolower($search);
673
674
675
676 if ($option_limits[$detail_id] > 0 && 0 >= $option_remaining[$detail_id]) {
677 break;
678 }
679
680 if ($term_limits[$detail_id] > 0 && array_key_exists($replace, $replaced_tags_counts) && $replaced_tags_counts[$replace] >= $term_limits[$detail_id]) {
681 continue;
682 }
683
684 if ($term_limits[$detail_id] > 0 && array_key_exists($replace, $replaced_tags_counts)) {
685 $same_usage_max = min($term_limits[$detail_id] - $replaced_tags_counts[$replace], $option_remaining[$detail_id]);
686 } else {
687 $same_usage_max = min($term_limits[$detail_id], $option_remaining[$detail_id]);
688 }
689
690 // Replace HTML entities with placeholders in term name too to match them in content
691 $search = preg_replace_callback('/&#(\d+);/', function($matches) {
692 return 'STARTTAXOPRESSENTITY' . $matches[1] . 'TAXOPRESSENTITYEND';
693 }, $search);
694
695 $whole_words = isset($options['whole_words']) ? (int)$options['whole_words'] : 1;
696 if ($whole_words) {
697 $pattern = '/\b' . preg_quote($search, "/") . '\b/ui';
698 } else {
699 // Allow partial matches - no word boundary checks
700 $pattern = '/' . preg_quote($search, "/") . '/i';
701 }
702
703 //if ('i' === $case) {
704 if ($autolink_case === 'none') { // retain case
705 $replaced = preg_replace_callback($pattern, function($matches) use ($link_openeing, $link_closing) {
706 return $link_openeing . htmlspecialchars($matches[0]) . $link_closing;
707 }, $node->wholeText, $same_usage_max, $rep_count);
708 } elseif ($autolink_case === 'uppercase') { // uppercase
709 $replaced = preg_replace_callback($pattern, function($matches) use ($link_openeing, $upperterm, $link_closing) {
710 return $link_openeing . strtoupper($matches[0]) . $link_closing;
711 }, $node->wholeText, $same_usage_max, $rep_count);
712 } elseif ($autolink_case === 'termcase') { // termcase
713 $replaced = preg_replace_callback($pattern, function($matches) use ($link_openeing, $search, $link_closing) {
714 return $link_openeing . $search . $link_closing;
715 }, $node->wholeText, $same_usage_max, $rep_count);
716 } else { // lowercase
717 $replaced = preg_replace_callback($pattern, function($matches) use ($link_openeing, $lowerterm, $link_closing) {
718 return $link_openeing . strtolower($matches[0]) . $link_closing;
719 }, $node->wholeText, $same_usage_max, $rep_count);
720 }
721
722 if ($replaced && !empty(trim($replaced))) {
723 $j++;
724 if ($rep_count > 0) {
725 // TODO : Think about synonyms
726 if (array_key_exists($replace, $replaced_tags_counts)) {
727 $replaced_tags_counts[$replace] = $replaced_tags_counts[$replace] + $rep_count;
728 } else {
729 $replaced_tags_counts[$replace] = $rep_count;
730 }
731 $option_tagged_counts[$detail_id] = $option_tagged_counts[$detail_id] + $rep_count;
732 $option_remaining[$detail_id] = $option_limits[$detail_id] - $option_tagged_counts[$detail_id];
733 }
734 }
735 $newNode = $dom->createDocumentFragment();
736 $newNode->appendXML($replaced);
737
738 $node->parentNode->replaceChild($newNode, $node);
739 if ($option_remaining[$detail_id] === 0) {
740 break;
741 }
742 }
743 }
744
745
746 // Get the innerHTML of the root div, excluding the div itself
747 $content = '';
748 foreach ($dom->documentElement->childNodes as $node) {
749 $content .= $dom->saveHTML($node);
750 }
751
752 // Add back the starting "&#"
753 $content = str_replace('STARTTAXOPRESSENTITY', '&#', $content);
754 // Add back the ending ";"
755 $content = str_replace('TAXOPRESSENTITYEND', ';', $content);
756
757 // get only the body tag with its contents, then trim the body tag itself to get only the original content
758 //$content = mb_substr($dom->saveHTML($xpath->query('//body')->item(0)), 6, -7, "UTF-8");
759 $content = str_replace('|--|', '&#', $content); //https://github.com/TaxoPress/TaxoPress/issues/824
760 /**
761 * I commented the line below because of https://github.com/TaxoPress/TaxoPress/issues/2118
762 * In summary, when content contain < and > special character which are intentiona;, they're been
763 * changed to < > which is not needed
764 */
765 //$content = str_replace('&#60;', '<', $content);
766 //$content = str_replace('&#62;', '>', $content);
767
768 foreach (taxopress_html_character_and_entity(true) as $enity => $code) {
769 $content = str_replace($enity, $code, $content);
770 }
771
772 $content = str_replace('&amp ;rsquo;', '&rsquo;', $content);
773 $content = str_replace(['', '', '&rsquor;', ' &rsquor;', '&rsquo;', ' &rsquo;'], '\'', $content);
774
775 $content = str_replace('&#38;', '&', $content); //https://github.com/TaxoPress/TaxoPress/issues/770
776 $content = str_replace(';amp;', ';', $content); //https://github.com/TaxoPress/TaxoPress/issues/810
777 $content = str_replace('%7C--%7C038;', '&', $content); //https://github.com/TaxoPress/TaxoPress/issues/1377
778
779 $content = str_replace('starttaxopressrandom', '', $content);
780 $content = str_replace('endtaxopressrandom', '', $content);
781 // replace <taxopressnotag> added to skip certain elements
782 $content = str_replace('<taxopressnotag>', '', $content);
783 $content = str_replace('</taxopressnotag>', '', $content);
784
785 }
786
787 /**
788 * Replace text by link, except HTML tag, and already text into link, use PregEXP.
789 *
790 * @param string $content
791 * @param string $search
792 * @param string $replace
793 * @param string $case
794 * @param string $rel
795 */
796 private static function replace_by_links_regexp(&$content, $search = '', $replace = '', $case = '', $rel = '', $options = false)
797 {
798
799 if ($options) {
800 $autolink_case = $options['autolink_case'];
801 $html_exclusion = $options['html_exclusion'];
802 $html_exclusion_customs = isset($options['html_exclusion_customs']) ? $options['html_exclusion_customs'] : [];
803 $exclude_class = $options['autolink_exclude_class'];
804 $title_attribute = $options['autolink_title_attribute'];
805 $title_attribute_custom_url = $options['autolink_title_attribute_when_using_custom_url'];
806 $same_usage_max = $options['autolink_same_usage_max'];
807 $max_by_post = $options['autolink_usage_max'];
808 $link_class = isset($options['link_class']) ? taxopress_format_class($options['link_class']) : '';
809 } else {
810 $autolink_case = 'lowercase';
811 $html_exclusion = [];
812 $html_exclusion_customs = [];
813 $exclude_class = '';
814 $title_attribute = SimpleTags_Plugin::get_option_value('auto_link_title');
815 $title_attribute_custom_url = SimpleTags_Plugin::get_option_value('auto_link_title_custom_url');
816 $same_usage_max = SimpleTags_Plugin::get_option_value('auto_link_max_by_tag');
817 $max_by_post = SimpleTags_Plugin::get_option_value('auto_link_max_by_post');
818 $link_class = '';
819 }
820
821
822 if (!empty($html_exclusion_customs)) {
823 $html_exclusion = array_merge($html_exclusion, $html_exclusion_customs);
824 }
825
826 $must_tokenize = true; // will perform basic tokenization
827 $tokens = null; // two kinds of tokens: markup and text
828
829 $j = 0;
830 $filtered = ''; // will filter text token by token
831
832 // Get URL and title first
833 $url = array_key_exists($search, self::$link_tags) ? self::$link_tags[$search] : $replace;
834 $used_title = self::taxopress_get_title_attribute($search, $url, $options);
835 $replace = $url;
836
837 // Determine word boundary pattern based on whole_words setting
838 $whole_words = isset($options['whole_words']) ? (int)$options['whole_words'] : 1;
839 if ($whole_words) {
840 // Use \b for strict whole word matching
841 $match = '/\b' . preg_quote($search, '/') . '\b/u' . $case;
842 $substitute = '<a href="' . $replace . '" class="st_tag internal_tag ' . $link_class . '" ' . $rel . ' title="' . $used_title . "\">$0</a>";
843 } else {
844 // Allow partial matches - no word boundary checks
845 $match = '/(' . preg_quote($search, '/') . ')/u' . $case;
846 $substitute = '<a href="' . $replace . '" class="st_tag internal_tag ' . $link_class . '" ' . $rel . ' title="' . $used_title . "\">$1</a>";
847 }
848
849 //$match = "/\b" . preg_quote($search, "/") . "\b/".$case;
850 //$substitute = '<a href="'.$replace.'" class="st_tag internal_tag '.$link_class.'" '.$rel.' title="'. esc_attr( sprintf( __('Posts tagged with %s', 'simple-tags'), $search ) )."\">$0</a>";
851 // for efficiency only tokenize if forced to do so
852 if ($must_tokenize) {
853 // this regexp is taken from PHP Markdown by Michel Fortin: http://www.michelf.com/projects/php-markdown/
854 $comment = '(?s:<!(?:--.*?--\s*)+>)|';
855 $processing_instruction = '(?s:<\?.*?\?>)|';
856 $tag = '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)';
857
858 $markup = $comment . $processing_instruction . $tag;
859 $flags = PREG_SPLIT_DELIM_CAPTURE;
860 $tokens = preg_split("{($markup)}", $content, -1, $flags);
861 $must_tokenize = false;
862 }
863
864 // there should always be at least one token, but check just in case
865 $anchor_level = 0;
866
867 if (isset($tokens) && is_array($tokens) && count($tokens) > 0) {
868 $i = 0;
869 $ancestor = '';
870 foreach ($tokens as $token) {
871 if (++$i % 2 && $token !== '') { // this token is (non-markup) text
872
873
874 $pass_check = true;
875
876 if (!empty(trim($ancestor))) {
877
878 //auto link exclusion
879 if (count($html_exclusion) > 0) {
880 foreach ($html_exclusion as $exclude_ancestor) {
881 if (taxopress_starts_with($ancestor, '<' . strtolower($exclude_ancestor) . '')) {
882 $pass_check = false;
883 break;
884 }
885 }
886 }
887
888
889 // Prepare exclude terms array
890 $excludes_class = explode(',', $exclude_class);
891 if (!empty($excludes_class)) {
892 $excludes_class = array_filter($excludes_class);
893 $excludes_class = array_unique($excludes_class);
894 if (count($excludes_class) > 0) {
895 foreach ($excludes_class as $idclass) {
896 if (substr(trim($idclass), 0, 1) === "#") {
897 $div_id = ltrim(trim($idclass), "#");
898 if (preg_match_all('/<[a-z \'"]*id="' . $div_id . '"/i', $ancestor, $matches) || preg_match_all('/<[a-z \'"]*id=\'' . $div_id . '\'/i', $ancestor, $matches)) {
899 $pass_check = false;
900 break;
901 }
902 } else {
903 $div_class = ltrim(trim($idclass), ".");
904 if (preg_match_all('/<[a-z ]*class="' . $div_class . '"/i', $ancestor, $matches) || preg_match_all('/<[a-z ]*class=\'' . $div_class . '\'/i', $ancestor, $matches)) {
905 $pass_check = false;
906 break;
907 }
908 }
909 }
910 }
911 }
912 }
913 if ($anchor_level === 0 && $pass_check) { // linkify if not inside anchor tags
914 if (preg_match($match, $token)) { // use preg_match for compatibility with PHP 4
915 $j++;
916
917
918 $remaining_usage = $max_by_post - self::$tagged_link_count;
919 if ($same_usage_max > $remaining_usage) {
920 $same_usage_max = $remaining_usage;
921 }
922
923
924 if ($same_usage_max > 0) { // Limit replacement at 1 by default, or options value !
925 $token = preg_replace($match, $substitute, $token, $same_usage_max, $rep_count); // only PHP 5 supports calling preg_replace with 5 arguments
926 self::$tagged_link_count = self::$tagged_link_count + $rep_count;
927 }
928 $must_tokenize = true; // re-tokenize next time around
929 }
930 }
931 } else { // this token is markup
932 if (preg_match("#<\s*a\s+[^>]*>#i", $token)) { // found <a ...>
933 $ancestor = $token;
934 $anchor_level++;
935 } elseif (preg_match("#<\s*/\s*a\s*>#i", $token)) { // found </a>
936 $anchor_level--;
937 } elseif (taxopress_starts_with($token, "</")) {
938 $ancestor = '';
939 } else {
940 $ancestor = $token;
941 }
942 }
943 $filtered .= $token; // this token has now been filtered
944 }
945 $content = $filtered; // filtering completed for this link
946 }
947 }
948
949
950 /**
951 * Replace text by link to tag
952 *
953 * @param string $content
954 *
955 * @return string
956 */
957 public static function taxopress_autolinks_the_content($content = '')
958 {
959 global $post;
960
961
962 // Some Elementor render flows may call filters with no global $post set.
963 // Try to recover a post object before bailing out.
964 if (!is_object($post)) {
965 $post = get_post();
966 }
967
968
969 if (!is_object($post) || is_admin()) {
970 return $content;
971 }
972
973 $post_tags = taxopress_get_autolink_data();
974
975 // user preference for this post ?
976 $meta_value = get_post_meta($post->ID, '_exclude_autolinks', true);
977 if (!empty($meta_value)) {
978 return $content;
979 }
980
981 if (count($post_tags) > 0) {
982 $auto_link_replace = [];
983 foreach ($post_tags as $post_tag) {
984
985 // Get option
986 $embedded = (isset($post_tag['embedded']) && is_array($post_tag['embedded']) && count($post_tag['embedded']) > 0) ? $post_tag['embedded'] : false;
987
988 if (!$embedded) {
989 continue;
990 }
991
992 if (!in_array($post->post_type, $embedded)) {
993 continue;
994 }
995
996 if ($post_tag['autolink_display'] === 'post_title') {
997 continue;
998 }
999
1000 //reset tags just in case
1001 self::$link_tags = [];
1002 // Get currents tags if no exists
1003 self::prepare_auto_link_tags($post_tag);
1004
1005 // Shuffle array
1006 SimpleTags_Client::random_array(self::$link_tags);
1007
1008 // HTML Rel (tag/no-follow)
1009 $rel = SimpleTags_Client::get_rel_attribut();
1010
1011 // only continue if the database actually returned any links
1012 if (!isset(self::$link_tags) || !is_array(self::$link_tags) || empty(self::$link_tags)) {
1013 $can_continue = false;
1014 } else {
1015 $can_continue = true;
1016 }
1017
1018 if ($can_continue) {
1019 // Case option ?
1020 $case = (1 === (int) $post_tag['ignore_case']) ? 'i' : '';
1021 $strpos_fnc = ('i' === $case) ? 'stripos' : 'strpos';
1022
1023 // Prepare exclude terms array
1024 $excludes_terms = explode(',', $post_tag['auto_link_exclude']);
1025 if (empty($excludes_terms)) {
1026 $excludes_terms = array();
1027 } else {
1028 $excludes_terms = array_filter($excludes_terms, '_delete_empty_element');
1029 $excludes_terms = array_unique($excludes_terms);
1030 }
1031
1032 $z = 0;
1033
1034 foreach ((array) self::$link_tags as $term_name => $term_link) {
1035 $z++;
1036 // Force string for tags "number"
1037 $term_name = (string) $term_name;
1038
1039 // Exclude terms ? next...
1040 if (taxopress_in_array_i($term_name, (array) $excludes_terms, true)) {
1041 continue;
1042 }
1043
1044 // Make a first test with PHP function, economize CPU with regexp
1045 if (false === $strpos_fnc($post->post_content, $term_name)) {
1046 continue;
1047 }
1048
1049 $auto_link_replace[] = [
1050 'term_name' => $term_name,
1051 'term_link' => $term_link,
1052 'case' => $case,
1053 'rel' => $rel,
1054 'options' => $post_tag,
1055 'option_id' => $post_tag['ID'],
1056 'post_limit' => $post_tag['autolink_usage_max'],
1057 'term_limit' => $post_tag['autolink_same_usage_max'],
1058 'type' => 'content',
1059 ];
1060
1061 if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {
1062 self::replace_by_links_regexp($content, $term_name, $term_link, $case, $rel, $post_tag);
1063 }
1064 }
1065 }
1066 }
1067 if (class_exists('DOMDocument') && class_exists('DOMXPath')) {
1068 self::replace_by_links_dom($content, $auto_link_replace);
1069 }
1070 }
1071 return $content;
1072 }
1073
1074
1075
1076
1077 /**
1078 * Replace text by link to tag
1079 *
1080 * @param string $title
1081 *
1082 * @return string
1083 */
1084 public static function taxopress_autolinks_the_title($title = '')
1085 {
1086 global $post;
1087
1088 if (!is_object($post) || is_admin()) {
1089 return $title;
1090 }
1091
1092 $post_tags = taxopress_get_autolink_data();
1093
1094
1095 // user preference for this post ?
1096 $meta_value = get_post_meta($post->ID, '_exclude_autolinks', true);
1097 if (!empty($meta_value)) {
1098 return $title;
1099 }
1100
1101 if (count($post_tags) > 0) {
1102
1103 foreach ($post_tags as $post_tag) {
1104
1105 // Get option
1106 $embedded = (isset($post_tag['embedded']) && is_array($post_tag['embedded']) && count($post_tag['embedded']) > 0) ? $post_tag['embedded'] : false;
1107
1108 if (!$embedded) {
1109 continue;
1110 }
1111
1112 if (!in_array($post->post_type, $embedded)) {
1113 continue;
1114 }
1115
1116 if ($post_tag['autolink_display'] === 'post_content') {
1117 continue;
1118 }
1119 //reset tags just in case
1120 self::$link_tags = [];
1121 // Get currents tags if no exists
1122 self::prepare_auto_link_tags($post_tag);
1123
1124 // Shuffle array
1125 SimpleTags_Client::random_array(self::$link_tags);
1126
1127 // HTML Rel (tag/no-follow)
1128 $rel = SimpleTags_Client::get_rel_attribut();
1129
1130 // only continue if the database actually returned any links
1131 if (!isset(self::$link_tags) || !is_array(self::$link_tags) || empty(self::$link_tags)) {
1132 $can_continue = false;
1133 } else {
1134 $can_continue = true;
1135 }
1136
1137 if ($can_continue) {
1138
1139 // Case option ?
1140 $case = (1 === (int) $post_tag['ignore_case']) ? 'i' : '';
1141 $strpos_fnc = ('i' === $case) ? 'stripos' : 'strpos';
1142
1143 // Prepare exclude terms array
1144 $excludes_terms = explode(',', $post_tag['auto_link_exclude']);
1145 if (empty($excludes_terms)) {
1146 $excludes_terms = array();
1147 } else {
1148 $excludes_terms = array_filter($excludes_terms, '_delete_empty_element');
1149 $excludes_terms = array_unique($excludes_terms);
1150 }
1151
1152 $z = 0;
1153 $auto_link_replace = [];
1154 foreach ((array) self::$link_tags as $term_name => $term_link) {
1155 $z++;
1156 // Force string for tags "number"
1157 $term_name = (string) $term_name;
1158
1159 // Exclude terms ? next...
1160 if (taxopress_in_array_i($term_name, (array) $excludes_terms, true)) {
1161 continue;
1162 }
1163
1164 // Make a first test with PHP function, economize CPU with regexp
1165 if (false === $strpos_fnc($title, $term_name)) {
1166 continue;
1167 }
1168
1169 $auto_link_replace[] = [
1170 'term_name' => $term_name,
1171 'term_link' => $term_link,
1172 'case' => $case,
1173 'rel' => $rel,
1174 'options' => $post_tag,
1175 'option_id' => $post_tag['ID'],
1176 'post_limit' => $post_tag['autolink_usage_max'],
1177 'term_limit' => $post_tag['autolink_same_usage_max'],
1178 'type' => 'content',
1179 ];
1180
1181 if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {
1182 self::replace_by_links_regexp($title, $term_name, $term_link, $case, $rel, $post_tag);
1183 }
1184 }
1185 if (class_exists('DOMDocument') && class_exists('DOMXPath')) {
1186 self::replace_by_links_dom($title, $auto_link_replace);
1187 }
1188 }
1189 }
1190 }
1191
1192
1193 return $title;
1194 }
1195 }
1196