PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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-emoji.php

class-emoji.php in ActivityPub 8.0.2, at includes/class-emoji.php

272 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ActivityPub Emoji file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 /**
11 * Handles custom emoji processing for ActivityPub content.
12 *
13 * Wraps emoji shortcodes with block patterns at insert time. The blocks are
14 * rendered at display time by WordPress (posts) or via do_blocks() (comments).
15 *
16 * Also handles emoji replacement for comment author names (which don't use blocks).
17 *
18 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/9098/fep-9098.md FEP-9098: Custom Emojis
19 */
20 class Emoji {
21
22 /**
23 * Wrap emoji shortcodes in content with block patterns.
24 *
25 * Called at insert time to wrap emoji shortcodes with activitypub/emoji blocks.
26 * The blocks are rendered at display time via their render_callback.
27 *
28 * @param string $content The content to process.
29 * @param array $activity The activity containing emoji definitions in 'tag'.
30 *
31 * @return string The content with wrapped emoji.
32 */
33 public static function wrap_in_content( $content, $activity ) {
34 if ( empty( $content ) || empty( $activity['tag'] ) || ! \is_array( $activity['tag'] ) ) {
35 return $content;
36 }
37
38 foreach ( $activity['tag'] as $tag ) {
39 if ( ! \is_array( $tag ) || ! isset( $tag['type'] ) || 'Emoji' !== $tag['type'] || empty( $tag['name'] ) ) {
40 continue;
41 }
42
43 $url = object_to_uri( $tag['icon'] ?? null );
44 if ( empty( $url ) ) {
45 continue;
46 }
47
48 $shortcode = $tag['name'];
49 $block_attrs = array( 'url' => \esc_url( $url ) );
50
51 if ( ! empty( $tag['updated'] ) ) {
52 $block_attrs['updated'] = $tag['updated'];
53 }
54
55 $wrapped = \sprintf(
56 '<!-- wp:activitypub/emoji %s -->%s<!-- /wp:activitypub/emoji -->',
57 \wp_json_encode( $block_attrs ),
58 $shortcode
59 );
60
61 // Case-insensitive replacement, avoid already wrapped shortcodes.
62 $pattern = '/(?<!-->)' . \preg_quote( $shortcode, '/' ) . '(?!<!-- \/wp:activitypub\/emoji -->)/i';
63 $content = \preg_replace( $pattern, $wrapped, $content );
64 }
65
66 return $content;
67 }
68
69 /**
70 * Generate an emoji img tag.
71 *
72 * @param string $url The emoji image URL.
73 * @param string $name The emoji name (without colons).
74 *
75 * @return string The emoji img tag HTML.
76 */
77 public static function get_img_tag( $url, $name ) {
78 return \sprintf(
79 '<img src="%s" alt="%s" title="%s" class="emoji" width="20" height="20" draggable="false" />',
80 \esc_url( $url ),
81 \esc_attr( $name ),
82 \esc_attr( $name )
83 );
84 }
85
86 /**
87 * Get the allowed HTML structure for emoji img tags.
88 *
89 * Used by Comment class for KSES validation of emoji in author names.
90 *
91 * @return array The allowed HTML structure for use with wp_kses.
92 */
93 public static function get_kses_allowed_html() {
94 return array(
95 'img' => array(
96 'class' => array(
97 'required' => true,
98 'values' => array( 'emoji' ),
99 ),
100 'src' => array(
101 'required' => true,
102 'value_callback' => array( self::class, 'validate_emoji_src' ),
103 ),
104 'alt' => array( 'required' => true ),
105 'title' => array( 'required' => true ),
106 'height' => array(
107 'required' => true,
108 'values' => array( '20' ),
109 ),
110 'width' => array(
111 'required' => true,
112 'values' => array( '20' ),
113 ),
114 'draggable' => array(
115 'required' => true,
116 'values' => array( 'false' ),
117 ),
118 ),
119 );
120 }
121
122 /**
123 * Validate emoji src attribute for wp_kses.
124 *
125 * By default, only allows locally cached emoji URLs for privacy.
126 * Remote URLs are only allowed when caching is explicitly disabled.
127 *
128 * @param string $value The src attribute value.
129 *
130 * @return bool True if the src is valid, false otherwise.
131 */
132 public static function validate_emoji_src( $value ) {
133 $upload_dir = \wp_upload_dir();
134 $emoji_base = $upload_dir['baseurl'] . '/activitypub/emoji/';
135
136 // Allow local cached emoji.
137 if ( \str_starts_with( $value, $emoji_base ) ) {
138 return true;
139 }
140
141 // Only allow remote URLs when caching is explicitly disabled.
142 // This protects user privacy by defaulting to local-only emoji.
143 $allow_remote = ! Cache::is_enabled();
144
145 // Validate the URL format if remote is allowed.
146 if ( $allow_remote ) {
147 $allow_remote = (bool) \wp_http_validate_url( $value );
148 }
149
150 /**
151 * Filters whether a remote emoji URL is valid.
152 *
153 * Use this filter to explicitly allow remote emoji URLs when needed
154 * (e.g., for CDN proxying).
155 *
156 * @since 5.6.0
157 *
158 * @param bool $valid Whether the URL is valid.
159 * @param string $value The emoji src URL.
160 */
161 return \apply_filters( 'activitypub_validate_emoji_src', $allow_remote, $value );
162 }
163
164 /**
165 * Prepare actor meta for emoji storage.
166 *
167 * Used for storing actor emoji data for comment author name rendering.
168 *
169 * @param array $actor The actor array containing potential emoji in tags.
170 *
171 * @return array Meta input array with emoji data, or empty array if no emoji.
172 */
173 public static function prepare_actor_meta( $actor ) {
174 if ( empty( $actor['tag'] ) || ! \is_array( $actor['tag'] ) ) {
175 return array();
176 }
177
178 $emoji_tags = \array_values(
179 \array_filter(
180 $actor['tag'],
181 function ( $tag ) {
182 return \is_array( $tag ) && isset( $tag['type'] ) && 'Emoji' === $tag['type'];
183 }
184 )
185 );
186
187 if ( empty( $emoji_tags ) ) {
188 return array();
189 }
190
191 return array(
192 '_activitypub_emoji' => \wp_json_encode( $emoji_tags ),
193 );
194 }
195
196 /**
197 * Replace emoji from stored JSON data.
198 *
199 * Used for comment author name replacement at display time.
200 *
201 * @param string $text The text to process.
202 * @param string $emoji_json JSON-encoded emoji tag data.
203 *
204 * @return string The processed text with emoji replacements.
205 */
206 public static function replace_from_json( $text, $emoji_json ) {
207 $tags = \json_decode( $emoji_json, true );
208
209 if ( empty( $tags ) || ! \is_array( $tags ) ) {
210 return $text;
211 }
212
213 foreach ( $tags as $tag ) {
214 if ( empty( $tag['name'] ) ) {
215 continue;
216 }
217
218 $url = object_to_uri( $tag['icon'] ?? null );
219 if ( empty( $url ) ) {
220 continue;
221 }
222
223 /**
224 * Filters a remote media URL for caching.
225 *
226 * @param string $url The remote media URL.
227 * @param string $context The context ('emoji').
228 * @param string|null $entity_id The entity ID.
229 * @param array $options Additional options.
230 */
231 $cached_url = \apply_filters(
232 'activitypub_remote_media_url',
233 $url,
234 'emoji',
235 null,
236 array( 'updated' => $tag['updated'] ?? null )
237 );
238
239 $name = \trim( $tag['name'], ':' );
240 $img = self::get_img_tag( $cached_url ?: $url, $name );
241
242 $text = \str_ireplace( $tag['name'], $img, $text );
243 }
244
245 return $text;
246 }
247
248 /**
249 * Replace emoji in text using a remote actor's stored emoji data.
250 *
251 * Used by Mailer class for actor name/summary in emails.
252 *
253 * @param string $text The text to process.
254 * @param string $actor_url The actor's URL to look up emoji data.
255 *
256 * @return string The processed text with emoji replacements.
257 */
258 public static function replace_for_actor( $text, $actor_url ) {
259 $actor_post = Collection\Remote_Actors::get_by_uri( $actor_url );
260 if ( ! $actor_post || \is_wp_error( $actor_post ) ) {
261 return $text;
262 }
263
264 $emoji_data = \get_post_meta( $actor_post->ID, '_activitypub_emoji', true );
265 if ( empty( $emoji_data ) ) {
266 return $text;
267 }
268
269 return self::replace_from_json( $text, $emoji_data );
270 }
271 }
272