PluginProbe
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms / 3.40.0
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms v3.40.0
3.54.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 All 178 releases
simple-tags / inc / class.client.autolinks.php

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

1,177 lines 40.6 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('wp_head', [__CLASS__, 'print_autolink_inline_style']);
41
42 add_action('admin_init', [$this, 'taxopress_customurl_taxonomies_fields']);
43 }
44 }
45
46 public function taxopress_customurl_taxonomies_fields() {
47
48 if (!$this->is_taxonomy_page()) {
49 return;
50 }
51
52 // Cache the autolink settings to avoid repeated database calls
53 static $enabled_taxonomies = null;
54
55 if ($enabled_taxonomies === null) {
56 $autolink_settings = taxopress_get_autolink_data();
57 $enabled_taxonomies = $this->get_enabled_taxonomies($autolink_settings);
58 }
59
60 // Only get taxonomies that are actually enabled
61 $taxonomies = get_taxonomies([], 'objects');
62
63 foreach ($enabled_taxonomies as $taxonomy_name) {
64 if (isset($taxonomies[$taxonomy_name])) {
65 add_action("{$taxonomy_name}_edit_form_fields", [$this, 'taxopress_add_custom_url_field']);
66 add_action("{$taxonomy_name}_add_form_fields", [$this, 'taxopress_add_custom_url_field_new']);
67 add_action("edited_{$taxonomy_name}", [$this, 'taxopress_save_custom_url_field']);
68 add_action("created_{$taxonomy_name}", [$this, 'taxopress_save_custom_url_field']);
69 }
70 }
71 }
72
73 private function is_taxonomy_page() {
74 global $pagenow;
75 return in_array($pagenow, ['edit-tags.php', 'term.php']);
76 }
77
78 private function get_enabled_taxonomies($autolink_settings) {
79 $default_enabled_taxonomies = ['post_tag', 'category'];
80 $enabled_taxonomies = [];
81 $has_setting_saved = false;
82
83 foreach ($autolink_settings as $setting) {
84 if (array_key_exists('enable_customurl_field', $setting)) {
85 $has_setting_saved = true;
86 $enabled_taxonomies = is_array($setting['enable_customurl_field']) ? $setting['enable_customurl_field'] : [];
87 break;
88 }
89 }
90
91 return $has_setting_saved ? array_unique($enabled_taxonomies) : $default_enabled_taxonomies;
92 }
93
94 public static function print_autolink_inline_style() {
95 // Ensure autolink anchors are visibly underlined on the frontend (overrides Elementor/theme rules)
96 echo '<style type="text/css">a.st_tag, a.internal_tag, .st_tag, .internal_tag { text-decoration: underline !important; }</style>';
97 }
98
99 /**
100 * Stock posts ID as soon as possible
101 * TODO: test if post_type allow post_tag before keep post ID
102 *
103 * @param array $posts
104 *
105 * @return array
106 */
107 public static function the_posts($posts)
108 {
109 if (!empty($posts) && is_array($posts)) {
110 foreach ((array) $posts as $post) {
111 self::$posts[] = (int) $post->ID;
112 }
113
114 self::$posts = array_unique(self::$posts);
115 }
116
117 return $posts;
118 }
119
120 /**
121 * Get tags from current post views
122 *
123 * @return array
124 */
125 public static function get_tags_from_current_posts($options = false)
126 {
127
128 if (is_array(self::$posts) && count(self::$posts) > 0) {
129 // Generate SQL from post id
130 $postlist = implode("', '", self::$posts);
131
132 // Generate key cache
133 $key = md5(maybe_serialize($postlist));
134
135 $results = array();
136
137 if ($options) {
138 $term_taxonomy = $options['taxonomy'];
139 } else {
140 $term_taxonomy = 'post_tag';
141 }
142
143 // Get cache if exist
144 $cache = wp_cache_get('generate_keywords', 'simple-tags');
145 if ($options || false === $cache) {
146 if ($cache === false) {
147 $cache = [];
148 }
149 foreach (self::$posts as $object_id) {
150 // Get terms
151 $terms = get_object_term_cache($object_id, $term_taxonomy);
152 if (false === $terms || is_wp_error($terms)) {
153 $terms = wp_get_object_terms($object_id, $term_taxonomy);
154 }
155
156 if (false !== $terms && !is_wp_error($terms)) {
157 $results = array_merge($results, $terms);
158 }
159 }
160
161 $cache[$key] = $results;
162 wp_cache_set('generate_keywords', $cache, 'simple-tags');
163 } else {
164 if (isset($cache[$key])) {
165 return $cache[$key];
166 }
167 }
168
169 return $results;
170 }
171
172 return array();
173 }
174
175 /**
176 * Get all available local tags
177 *
178 * @return array
179 */
180 public static function get_all_post_tags($options = false)
181 {
182 if (is_array(self::$posts) && count(self::$posts) > 0) {
183 // Generate SQL from post id
184 $postlist = implode("', '", self::$posts);
185
186 // Generate key cache
187 $key = md5(maybe_serialize($postlist));
188
189 if ($options) {
190 $term_taxonomy = $options['taxonomy'];
191 } else {
192 $term_taxonomy = 'post_tag';
193 }
194 $results = get_tags(['taxonomy' => $term_taxonomy, 'hide_empty' => false]);
195 // Get cache if exist
196 $cache = wp_cache_get('generate_keywords', 'simple-tags');
197 if ($options || false === $cache) {
198 foreach (self::$posts as $object_id) {
199 // Get terms
200 $terms = get_object_term_cache($object_id, $term_taxonomy);
201 if (false === $terms || is_wp_error($terms)) {
202 $terms = wp_get_object_terms($object_id, $term_taxonomy);
203 }
204
205 if (false !== $terms && !is_wp_error($terms)) {
206 $results = array_merge($results, $terms);
207 }
208 }
209 $cache = [];
210 $cache[$key] = $results;
211 wp_cache_set('generate_keywords', $cache, 'simple-tags');
212 } else {
213 if (isset($cache[$key])) {
214 return $cache[$key];
215 }
216 }
217
218 return $results;
219 }
220
221 return array();
222 }
223
224 public function taxopress_add_custom_url_field($term) {
225
226 if (!is_object($term)) {
227 return;
228 }
229
230 $taxopress_custom_url = get_term_meta($term->term_id, 'taxopress_custom_url', true);
231
232 ?>
233 <tr class="form-field">
234 <th scope="row" valign="top">
235 <label for="taxopress_custom_url"><?php esc_html_e('Custom URL', 'simple-tags'); ?></label>
236 </th>
237 <td>
238 <input type="text" name="taxopress_custom_url" id="taxopress_custom_url" value="<?php echo esc_attr($taxopress_custom_url); ?>" size="40">
239 <p class="description">
240 <?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'); ?>
241 </p>
242
243 </td>
244 </tr>
245 <?php
246 }
247
248 public function taxopress_add_custom_url_field_new() {
249 ?>
250 <div class="form-field">
251 <label for="taxopress_custom_url"><?php esc_html_e('Custom URL', 'simple-tags'); ?></label>
252 <input type="text" name="taxopress_custom_url" id="taxopress_custom_url" value="" size="40">
253 <p class="description">
254 <?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'); ?>
255 </p>
256 </div>
257 <?php
258 }
259
260 public function taxopress_save_custom_url_field($term_id) {
261 if (!empty($_POST['taxopress_custom_url'])) {
262 $taxopress_custom_url = sanitize_url(wp_unslash($_POST['taxopress_custom_url']));
263 update_term_meta($term_id, 'taxopress_custom_url', $taxopress_custom_url);
264 } else {
265 delete_term_meta($term_id, 'taxopress_custom_url');
266 }
267
268 }
269
270 /**
271 * Get links for each tag for auto link feature
272 *
273 */
274 public static function prepare_auto_link_tags($options = false)
275 {
276 global $post;
277 if ($options) {
278 $auto_link_min = (int) $options['autolink_usage_min'];
279 $unattached_terms = (int) $options['unattached_terms'];
280 $autolink_min_char = (int) $options['autolink_min_char'];
281 $autolink_max_char = (int) $options['autolink_max_char'];
282 $term_taxonomy = $options['taxonomy'];
283 $custom_urls_enabled = !empty($options['enable_custom_urls']);
284 } else {
285 $auto_link_min = (int) SimpleTags_Plugin::get_option_value('auto_link_min');
286 $unattached_terms = (int) SimpleTags_Plugin::get_option_value('auto_link_all');
287 $autolink_min_char = 0;
288 $autolink_max_char = 0;
289 $term_taxonomy = 'post_tag';
290 $custom_urls_enabled = false;
291 $autolink_settings = taxopress_get_autolink_data();
292 foreach ($autolink_settings as $setting) {
293 if (!empty($setting['enable_custom_urls'])) {
294 $custom_urls_enabled = true;
295 break;
296 }
297 }
298 }
299
300 if (1 === $unattached_terms) {
301 $terms = self::get_all_post_tags($options);
302 } else {
303 $terms = self::get_tags_from_current_posts($options);
304 }
305
306 $custom_urls_enabled = !empty($options['enable_custom_urls']);
307 $archivepage = !empty($options['archivepage']);
308
309 if (!$archivepage && !$custom_urls_enabled) {
310 self::$link_tags = [];
311 return true;
312 }
313
314 $custom_urls = [];
315 if ($custom_urls_enabled && !empty($terms)) {
316 $term_ids = wp_list_pluck($terms, 'term_id');
317 $custom_urls = self::get_term_meta_batch($term_ids, 'taxopress_custom_url');
318 }
319
320 foreach ((array) $terms as $term) {
321
322 //hidden terms should not be auto linked
323 if ((int) SimpleTags_Plugin::get_option_value('enable_hidden_terms') === 1) {
324 $min_usage = (int) SimpleTags_Plugin::get_option_value('hide-rarely');
325 if ($term->count < $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 //if ('i' === $case) {
696 if ($autolink_case === 'none') { // retain case
697 $replaced = preg_replace_callback('/(?<!\w)' . preg_quote($search, "/") . '(?!\w)/i', function($matches) use ($link_openeing, $link_closing) {
698 return $link_openeing . htmlspecialchars($matches[0]) . $link_closing;
699 }, $node->wholeText, $same_usage_max, $rep_count);
700 } elseif ($autolink_case === 'uppercase') { // uppercase
701 $replaced = preg_replace_callback('/(?<!\w)' . preg_quote($search, "/") . '(?!\w)/i', function($matches) use ($link_openeing, $upperterm, $link_closing) {
702 return $link_openeing . strtoupper($matches[0]) . $link_closing;
703 }, $node->wholeText, $same_usage_max, $rep_count);
704 } elseif ($autolink_case === 'termcase') { // termcase
705 $replaced = preg_replace_callback('/(?<!\w)' . preg_quote($search, "/") . '(?!\w)/i', function($matches) use ($link_openeing, $search, $link_closing) {
706 return $link_openeing . $search . $link_closing;
707 }, $node->wholeText, $same_usage_max, $rep_count);
708 } else { // lowercase
709 $replaced = preg_replace_callback('/(?<!\w)' . preg_quote($search, "/") . '(?!\w)/i', function($matches) use ($link_openeing, $lowerterm, $link_closing) {
710 return $link_openeing . strtolower($matches[0]) . $link_closing;
711 }, $node->wholeText, $same_usage_max, $rep_count);
712 }
713
714 if ($replaced && !empty(trim($replaced))) {
715 $j++;
716 if ($rep_count > 0) {
717 // TODO : Think about synonyms
718 if (array_key_exists($replace, $replaced_tags_counts)) {
719 $replaced_tags_counts[$replace] = $replaced_tags_counts[$replace] + $rep_count;
720 } else {
721 $replaced_tags_counts[$replace] = $rep_count;
722 }
723 $option_tagged_counts[$detail_id] = $option_tagged_counts[$detail_id] + $rep_count;
724 $option_remaining[$detail_id] = $option_limits[$detail_id] - $option_tagged_counts[$detail_id];
725 }
726 }
727 $newNode = $dom->createDocumentFragment();
728 $newNode->appendXML($replaced);
729
730 $node->parentNode->replaceChild($newNode, $node);
731 if ($option_remaining[$detail_id] === 0) {
732 break;
733 }
734 }
735 }
736
737
738 // Get the innerHTML of the root div, excluding the div itself
739 $content = '';
740 foreach ($dom->documentElement->childNodes as $node) {
741 $content .= $dom->saveHTML($node);
742 }
743
744 // Add back the starting "&#"
745 $content = str_replace('STARTTAXOPRESSENTITY', '&#', $content);
746 // Add back the ending ";"
747 $content = str_replace('TAXOPRESSENTITYEND', ';', $content);
748
749 // get only the body tag with its contents, then trim the body tag itself to get only the original content
750 //$content = mb_substr($dom->saveHTML($xpath->query('//body')->item(0)), 6, -7, "UTF-8");
751 $content = str_replace('|--|', '&#', $content); //https://github.com/TaxoPress/TaxoPress/issues/824
752 /**
753 * I commented the line below because of https://github.com/TaxoPress/TaxoPress/issues/2118
754 * In summary, when content contain < and > special character which are intentiona;, they're been
755 * changed to < > which is not needed
756 */
757 //$content = str_replace('&#60;', '<', $content);
758 //$content = str_replace('&#62;', '>', $content);
759
760 foreach (taxopress_html_character_and_entity(true) as $enity => $code) {
761 $content = str_replace($enity, $code, $content);
762 }
763
764 $content = str_replace('&amp ;rsquo;', '&rsquo;', $content);
765 $content = str_replace(['’', ' ’', '&rsquor;', ' &rsquor;', '&rsquo;', ' &rsquo;'], '\'', $content);
766
767 $content = str_replace('&#38;', '&', $content); //https://github.com/TaxoPress/TaxoPress/issues/770
768 $content = str_replace(';amp;', ';', $content); //https://github.com/TaxoPress/TaxoPress/issues/810
769 $content = str_replace('%7C--%7C038;', '&', $content); //https://github.com/TaxoPress/TaxoPress/issues/1377
770
771 $content = str_replace('starttaxopressrandom', '', $content);
772 $content = str_replace('endtaxopressrandom', '', $content);
773 // replace <taxopressnotag> added to skip certain elements
774 $content = str_replace('<taxopressnotag>', '', $content);
775 $content = str_replace('</taxopressnotag>', '', $content);
776
777 }
778
779 /**
780 * Replace text by link, except HTML tag, and already text into link, use PregEXP.
781 *
782 * @param string $content
783 * @param string $search
784 * @param string $replace
785 * @param string $case
786 * @param string $rel
787 */
788 private static function replace_by_links_regexp(&$content, $search = '', $replace = '', $case = '', $rel = '', $options = false)
789 {
790
791 if ($options) {
792 $autolink_case = $options['autolink_case'];
793 $html_exclusion = $options['html_exclusion'];
794 $html_exclusion_customs = isset($options['html_exclusion_customs']) ? $options['html_exclusion_customs'] : [];
795 $exclude_class = $options['autolink_exclude_class'];
796 $title_attribute = $options['autolink_title_attribute'];
797 $title_attribute_custom_url = $options['autolink_title_attribute_when_using_custom_url'];
798 $same_usage_max = $options['autolink_same_usage_max'];
799 $max_by_post = $options['autolink_usage_max'];
800 $link_class = isset($options['link_class']) ? taxopress_format_class($options['link_class']) : '';
801 } else {
802 $autolink_case = 'lowercase';
803 $html_exclusion = [];
804 $html_exclusion_customs = [];
805 $exclude_class = '';
806 $title_attribute = SimpleTags_Plugin::get_option_value('auto_link_title');
807 $title_attribute_custom_url = SimpleTags_Plugin::get_option_value('auto_link_title_custom_url');
808 $same_usage_max = SimpleTags_Plugin::get_option_value('auto_link_max_by_tag');
809 $max_by_post = SimpleTags_Plugin::get_option_value('auto_link_max_by_post');
810 $link_class = '';
811 }
812
813
814 if (!empty($html_exclusion_customs)) {
815 $html_exclusion = array_merge($html_exclusion, $html_exclusion_customs);
816 }
817
818 $must_tokenize = true; // will perform basic tokenization
819 $tokens = null; // two kinds of tokens: markup and text
820
821 $j = 0;
822 $filtered = ''; // will filter text token by token
823
824 $match = '/(\PL|\A)(' . preg_quote($search, '/') . ')(\PL|\Z)\b/u' . $case;
825 $url = array_key_exists($search, self::$link_tags) ? self::$link_tags[$search] : $replace;
826 $used_title = self::taxopress_get_title_attribute($search, $url, $options);
827 $replace = $url;
828 $substitute = '$1<a href="' . $replace . '" class="st_tag internal_tag ' . $link_class . '" ' . $rel . ' title="' . $used_title . "\">$2</a>$3";
829
830 //$match = "/\b" . preg_quote($search, "/") . "\b/".$case;
831 //$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>";
832 // for efficiency only tokenize if forced to do so
833 if ($must_tokenize) {
834 // this regexp is taken from PHP Markdown by Michel Fortin: http://www.michelf.com/projects/php-markdown/
835 $comment = '(?s:<!(?:--.*?--\s*)+>)|';
836 $processing_instruction = '(?s:<\?.*?\?>)|';
837 $tag = '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)';
838
839 $markup = $comment . $processing_instruction . $tag;
840 $flags = PREG_SPLIT_DELIM_CAPTURE;
841 $tokens = preg_split("{($markup)}", $content, -1, $flags);
842 $must_tokenize = false;
843 }
844
845 // there should always be at least one token, but check just in case
846 $anchor_level = 0;
847
848 if (isset($tokens) && is_array($tokens) && count($tokens) > 0) {
849 $i = 0;
850 $ancestor = '';
851 foreach ($tokens as $token) {
852 if (++$i % 2 && $token !== '') { // this token is (non-markup) text
853
854
855 $pass_check = true;
856
857 if (!empty(trim($ancestor))) {
858
859 //auto link exclusion
860 if (count($html_exclusion) > 0) {
861 foreach ($html_exclusion as $exclude_ancestor) {
862 if (taxopress_starts_with($ancestor, '<' . strtolower($exclude_ancestor) . '')) {
863 $pass_check = false;
864 break;
865 }
866 }
867 }
868
869
870 // Prepare exclude terms array
871 $excludes_class = explode(',', $exclude_class);
872 if (!empty($excludes_class)) {
873 $excludes_class = array_filter($excludes_class);
874 $excludes_class = array_unique($excludes_class);
875 if (count($excludes_class) > 0) {
876 foreach ($excludes_class as $idclass) {
877 if (substr(trim($idclass), 0, 1) === "#") {
878 $div_id = ltrim(trim($idclass), "#");
879 if (preg_match_all('/<[a-z \'"]*id="' . $div_id . '"/i', $ancestor, $matches) || preg_match_all('/<[a-z \'"]*id=\'' . $div_id . '\'/i', $ancestor, $matches)) {
880 $pass_check = false;
881 break;
882 }
883 } else {
884 $div_class = ltrim(trim($idclass), ".");
885 if (preg_match_all('/<[a-z ]*class="' . $div_class . '"/i', $ancestor, $matches) || preg_match_all('/<[a-z ]*class=\'' . $div_class . '\'/i', $ancestor, $matches)) {
886 $pass_check = false;
887 break;
888 }
889 }
890 }
891 }
892 }
893 }
894 if ($anchor_level === 0 && $pass_check) { // linkify if not inside anchor tags
895 if (preg_match($match, $token)) { // use preg_match for compatibility with PHP 4
896 $j++;
897
898
899 $remaining_usage = $max_by_post - self::$tagged_link_count;
900 if ($same_usage_max > $remaining_usage) {
901 $same_usage_max = $remaining_usage;
902 }
903
904
905 if ($same_usage_max > 0) { // Limit replacement at 1 by default, or options value !
906 $token = preg_replace($match, $substitute, $token, $same_usage_max, $rep_count); // only PHP 5 supports calling preg_replace with 5 arguments
907 self::$tagged_link_count = self::$tagged_link_count + $rep_count;
908 }
909 $must_tokenize = true; // re-tokenize next time around
910 }
911 }
912 } else { // this token is markup
913 if (preg_match("#<\s*a\s+[^>]*>#i", $token)) { // found <a ...>
914 $ancestor = $token;
915 $anchor_level++;
916 } elseif (preg_match("#<\s*/\s*a\s*>#i", $token)) { // found </a>
917 $anchor_level--;
918 } elseif (taxopress_starts_with($token, "</")) {
919 $ancestor = '';
920 } else {
921 $ancestor = $token;
922 }
923 }
924 $filtered .= $token; // this token has now been filtered
925 }
926 $content = $filtered; // filtering completed for this link
927 }
928 }
929
930
931 /**
932 * Replace text by link to tag
933 *
934 * @param string $content
935 *
936 * @return string
937 */
938 public static function taxopress_autolinks_the_content($content = '')
939 {
940 global $post;
941
942
943 // Some Elementor render flows may call filters with no global $post set.
944 // Try to recover a post object before bailing out.
945 if (!is_object($post)) {
946 $post = get_post();
947 }
948
949
950 if (!is_object($post) || is_admin()) {
951 return $content;
952 }
953
954 $post_tags = taxopress_get_autolink_data();
955
956 // user preference for this post ?
957 $meta_value = get_post_meta($post->ID, '_exclude_autolinks', true);
958 if (!empty($meta_value)) {
959 return $content;
960 }
961
962 if (count($post_tags) > 0) {
963 $auto_link_replace = [];
964 foreach ($post_tags as $post_tag) {
965
966 // Get option
967 $embedded = (isset($post_tag['embedded']) && is_array($post_tag['embedded']) && count($post_tag['embedded']) > 0) ? $post_tag['embedded'] : false;
968
969 if (!$embedded) {
970 continue;
971 }
972
973 if (!in_array($post->post_type, $embedded)) {
974 continue;
975 }
976
977 if ($post_tag['autolink_display'] === 'post_title') {
978 continue;
979 }
980
981 //reset tags just in case
982 self::$link_tags = [];
983 // Get currents tags if no exists
984 self::prepare_auto_link_tags($post_tag);
985
986 // Shuffle array
987 SimpleTags_Client::random_array(self::$link_tags);
988
989 // HTML Rel (tag/no-follow)
990 $rel = SimpleTags_Client::get_rel_attribut();
991
992 // only continue if the database actually returned any links
993 if (!isset(self::$link_tags) || !is_array(self::$link_tags) || empty(self::$link_tags)) {
994 $can_continue = false;
995 } else {
996 $can_continue = true;
997 }
998
999 if ($can_continue) {
1000 // Case option ?
1001 $case = (1 === (int) $post_tag['ignore_case']) ? 'i' : '';
1002 $strpos_fnc = ('i' === $case) ? 'stripos' : 'strpos';
1003
1004 // Prepare exclude terms array
1005 $excludes_terms = explode(',', $post_tag['auto_link_exclude']);
1006 if (empty($excludes_terms)) {
1007 $excludes_terms = array();
1008 } else {
1009 $excludes_terms = array_filter($excludes_terms, '_delete_empty_element');
1010 $excludes_terms = array_unique($excludes_terms);
1011 }
1012
1013 $z = 0;
1014
1015 foreach ((array) self::$link_tags as $term_name => $term_link) {
1016 $z++;
1017 // Force string for tags "number"
1018 $term_name = (string) $term_name;
1019
1020 // Exclude terms ? next...
1021 if (taxopress_in_array_i($term_name, (array) $excludes_terms, true)) {
1022 continue;
1023 }
1024
1025 // Make a first test with PHP function, economize CPU with regexp
1026 if (false === $strpos_fnc($post->post_content, $term_name)) {
1027 continue;
1028 }
1029
1030 $auto_link_replace[] = [
1031 'term_name' => $term_name,
1032 'term_link' => $term_link,
1033 'case' => $case,
1034 'rel' => $rel,
1035 'options' => $post_tag,
1036 'option_id' => $post_tag['ID'],
1037 'post_limit' => $post_tag['autolink_usage_max'],
1038 'term_limit' => $post_tag['autolink_same_usage_max'],
1039 'type' => 'content',
1040 ];
1041
1042 if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {
1043 self::replace_by_links_regexp($content, $term_name, $term_link, $case, $rel, $post_tag);
1044 }
1045 }
1046 }
1047 }
1048 if (class_exists('DOMDocument') && class_exists('DOMXPath')) {
1049 self::replace_by_links_dom($content, $auto_link_replace);
1050 }
1051 }
1052 return $content;
1053 }
1054
1055
1056
1057
1058 /**
1059 * Replace text by link to tag
1060 *
1061 * @param string $title
1062 *
1063 * @return string
1064 */
1065 public static function taxopress_autolinks_the_title($title = '')
1066 {
1067 global $post;
1068
1069 if (!is_object($post) || is_admin()) {
1070 return $title;
1071 }
1072
1073 $post_tags = taxopress_get_autolink_data();
1074
1075
1076 // user preference for this post ?
1077 $meta_value = get_post_meta($post->ID, '_exclude_autolinks', true);
1078 if (!empty($meta_value)) {
1079 return $title;
1080 }
1081
1082 if (count($post_tags) > 0) {
1083
1084 foreach ($post_tags as $post_tag) {
1085
1086 // Get option
1087 $embedded = (isset($post_tag['embedded']) && is_array($post_tag['embedded']) && count($post_tag['embedded']) > 0) ? $post_tag['embedded'] : false;
1088
1089 if (!$embedded) {
1090 continue;
1091 }
1092
1093 if (!in_array($post->post_type, $embedded)) {
1094 continue;
1095 }
1096
1097 if ($post_tag['autolink_display'] === 'post_content') {
1098 continue;
1099 }
1100 //reset tags just in case
1101 self::$link_tags = [];
1102 // Get currents tags if no exists
1103 self::prepare_auto_link_tags($post_tag);
1104
1105 // Shuffle array
1106 SimpleTags_Client::random_array(self::$link_tags);
1107
1108 // HTML Rel (tag/no-follow)
1109 $rel = SimpleTags_Client::get_rel_attribut();
1110
1111 // only continue if the database actually returned any links
1112 if (!isset(self::$link_tags) || !is_array(self::$link_tags) || empty(self::$link_tags)) {
1113 $can_continue = false;
1114 } else {
1115 $can_continue = true;
1116 }
1117
1118 if ($can_continue) {
1119
1120 // Case option ?
1121 $case = (1 === (int) $post_tag['ignore_case']) ? 'i' : '';
1122 $strpos_fnc = ('i' === $case) ? 'stripos' : 'strpos';
1123
1124 // Prepare exclude terms array
1125 $excludes_terms = explode(',', $post_tag['auto_link_exclude']);
1126 if (empty($excludes_terms)) {
1127 $excludes_terms = array();
1128 } else {
1129 $excludes_terms = array_filter($excludes_terms, '_delete_empty_element');
1130 $excludes_terms = array_unique($excludes_terms);
1131 }
1132
1133 $z = 0;
1134 $auto_link_replace = [];
1135 foreach ((array) self::$link_tags as $term_name => $term_link) {
1136 $z++;
1137 // Force string for tags "number"
1138 $term_name = (string) $term_name;
1139
1140 // Exclude terms ? next...
1141 if (taxopress_in_array_i($term_name, (array) $excludes_terms, true)) {
1142 continue;
1143 }
1144
1145 // Make a first test with PHP function, economize CPU with regexp
1146 if (false === $strpos_fnc($title, $term_name)) {
1147 continue;
1148 }
1149
1150 $auto_link_replace[] = [
1151 'term_name' => $term_name,
1152 'term_link' => $term_link,
1153 'case' => $case,
1154 'rel' => $rel,
1155 'options' => $post_tag,
1156 'option_id' => $post_tag['ID'],
1157 'post_limit' => $post_tag['autolink_usage_max'],
1158 'term_limit' => $post_tag['autolink_same_usage_max'],
1159 'type' => 'content',
1160 ];
1161
1162 if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {
1163 self::replace_by_links_regexp($title, $term_name, $term_link, $case, $rel, $post_tag);
1164 }
1165 }
1166 if (class_exists('DOMDocument') && class_exists('DOMXPath')) {
1167 self::replace_by_links_dom($title, $auto_link_replace);
1168 }
1169 }
1170 }
1171 }
1172
1173
1174 return $title;
1175 }
1176 }
1177