PluginProbe
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms / 3.38.0
Tag, Category, and Taxonomy Manager – Autotagger Automatically Add Terms v3.38.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.38.0, at inc/class.client.autolinks.php

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