PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / class-hashtag.php

class-hashtag.php in ActivityPub trunk, at includes/class-hashtag.php

159 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Hashtag Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 /**
11 * ActivityPub Hashtag Class.
12 *
13 * @author Matthias Pfefferle
14 */
15 class Hashtag {
16 /**
17 * Initialize the class, registering WordPress hooks.
18 */
19 public static function init() {
20 if ( '1' === \get_option( 'activitypub_use_hashtags', '0' ) ) {
21 \add_action( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
22 \add_filter( 'the_content', array( self::class, 'the_content' ) );
23 \add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
24 }
25 }
26
27 /**
28 * Filter only the activity object and replace summery it with URLs.
29 *
30 * @param array $activity The activity object array.
31 *
32 * @return array The filtered activity object array.
33 */
34 public static function filter_activity_object( $activity ) {
35 if ( ! empty( $activity['summary'] ) && is_actor( $activity ) ) {
36 $activity['summary'] = self::the_content( $activity['summary'] );
37 }
38
39 if ( ! empty( $activity['content'] ) ) {
40 $activity['content'] = self::the_content( $activity['content'] );
41 }
42
43 return $activity;
44 }
45
46 /**
47 * Filter to save #tags as real WordPress tags.
48 *
49 * @param int $post_id Post ID.
50 * @param \WP_Post $post Post object.
51 */
52 public static function insert_post( $post_id, $post ) {
53 // Check if the post supports ActivityPub.
54 if ( ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) ) {
55 return;
56 }
57
58 // Check if the (custom) post supports tags.
59 $taxonomies = \get_object_taxonomies( $post );
60 if ( ! \in_array( 'post_tag', $taxonomies, true ) ) {
61 return;
62 }
63
64 $content = $post->post_content . "\n" . $post->post_excerpt;
65 $content = self::extract_text_outside_protected_tags( $content );
66
67 $tags = array();
68 if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $content, $match ) ) {
69 $tags = \array_unique( $match[1] );
70 }
71
72 \wp_add_post_tags( $post->ID, \implode( ', ', $tags ) );
73 }
74
75 /**
76 * Extract text content from outside protected HTML elements.
77 *
78 * Uses WP_HTML_Tag_Processor to properly parse HTML and skip content inside
79 * protected tags, matching the behavior of enrich_content_data().
80 *
81 * @param string $content The HTML content to process.
82 *
83 * @return string Text content from non-protected areas only.
84 */
85 private static function extract_text_outside_protected_tags( $content ) {
86 $processor = new \WP_HTML_Tag_Processor( $content );
87
88 /*
89 * Do not process content inside protected tags.
90 *
91 * Note: SCRIPT, STYLE, and TEXTAREA are "atomic" elements in
92 * WP_HTML_Tag_Processor, meaning their content is bundled with the tag
93 * token and won't appear as separate #text nodes. Because of this they
94 * do not need to be listed in $protected_tags: their inner text is
95 * never surfaced as #text tokens for us to process.
96 * See https://github.com/WordPress/wordpress-develop/blob/0fb3bb29596918864d808d156268a2df63c83620/src/wp-includes/html-api/class-wp-html-tag-processor.php#L276
97 */
98 $protected_tags = array( 'PRE', 'CODE', 'A' );
99 $tag_stack = array();
100 $filtered_content = '';
101
102 while ( $processor->next_token() ) {
103 $token_type = $processor->get_token_type();
104
105 if ( '#tag' === $token_type ) {
106 $tag_name = $processor->get_tag();
107
108 if ( $processor->is_tag_closer() ) {
109 // Closing tag: remove from stack.
110 $i = \array_search( $tag_name, $tag_stack, true );
111 if ( false !== $i ) {
112 $tag_stack = \array_slice( $tag_stack, 0, $i );
113 }
114 } elseif ( \in_array( $tag_name, $protected_tags, true ) ) {
115 // Opening tag: add to stack.
116 $tag_stack[] = $tag_name;
117 }
118 } elseif ( '#text' === $token_type && empty( $tag_stack ) ) {
119 // Only include text chunks that are outside protected tags.
120 $filtered_content .= $processor->get_modifiable_text();
121 }
122 }
123
124 return $filtered_content;
125 }
126
127 /**
128 * Filter to replace the #tags in the content with links.
129 *
130 * @param string $the_content The post content.
131 *
132 * @return string The filtered post content.
133 */
134 public static function the_content( $the_content ) {
135 return enrich_content_data( $the_content, '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( self::class, 'replace_with_links' ) );
136 }
137
138 /**
139 * A callback for preg_replace to build the term links.
140 *
141 * @param array $result The preg_match results.
142 * @return string the final string
143 */
144 public static function replace_with_links( $result ) {
145 $tag = $result[1];
146 $tag_object = \get_term_by( 'name', $tag, 'post_tag' );
147 if ( ! $tag_object ) {
148 $tag_object = \get_term_by( 'name', $tag, 'category' );
149 }
150
151 if ( $tag_object ) {
152 $link = \get_term_link( $tag_object, 'post_tag' );
153 return \sprintf( '<a rel="tag" class="hashtag u-tag u-category" href="%s">#%s</a>', \esc_url( $link ), $tag );
154 }
155
156 return '#' . $tag;
157 }
158 }
159