PluginProbe
ActivityPub / 9.0.1
ActivityPub v9.0.1
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 9.0.1, at includes/class-emoji.php

278 lines 7.4 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'] ) && \is_string( $tag['updated'] ) && \strtotime( $tag['updated'] ) ) {
52 $block_attrs['updated'] = \sanitize_text_field( $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_callback(
64 $pattern,
65 function () use ( $wrapped ) {
66 return $wrapped;
67 },
68 $content
69 );
70 }
71
72 return $content;
73 }
74
75 /**
76 * Generate an emoji img tag.
77 *
78 * @param string $url The emoji image URL.
79 * @param string $name The emoji name (without colons).
80 *
81 * @return string The emoji img tag HTML.
82 */
83 public static function get_img_tag( $url, $name ) {
84 return \sprintf(
85 '<img src="%s" alt="%s" title="%s" class="emoji" width="20" height="20" draggable="false" />',
86 \esc_url( $url ),
87 \esc_attr( $name ),
88 \esc_attr( $name )
89 );
90 }
91
92 /**
93 * Get the allowed HTML structure for emoji img tags.
94 *
95 * Used by Comment class for KSES validation of emoji in author names.
96 *
97 * @return array The allowed HTML structure for use with wp_kses.
98 */
99 public static function get_kses_allowed_html() {
100 return array(
101 'img' => array(
102 'class' => array(
103 'required' => true,
104 'values' => array( 'emoji' ),
105 ),
106 'src' => array(
107 'required' => true,
108 'value_callback' => array( self::class, 'validate_emoji_src' ),
109 ),
110 'alt' => array( 'required' => true ),
111 'title' => array( 'required' => true ),
112 'height' => array(
113 'required' => true,
114 'values' => array( '20' ),
115 ),
116 'width' => array(
117 'required' => true,
118 'values' => array( '20' ),
119 ),
120 'draggable' => array(
121 'required' => true,
122 'values' => array( 'false' ),
123 ),
124 ),
125 );
126 }
127
128 /**
129 * Validate emoji src attribute for wp_kses.
130 *
131 * By default, only allows locally cached emoji URLs for privacy.
132 * Remote URLs are only allowed when caching is explicitly disabled.
133 *
134 * @param string $value The src attribute value.
135 *
136 * @return bool True if the src is valid, false otherwise.
137 */
138 public static function validate_emoji_src( $value ) {
139 $upload_dir = \wp_upload_dir();
140 $emoji_base = $upload_dir['baseurl'] . '/activitypub/emoji/';
141
142 // Allow local cached emoji.
143 if ( \str_starts_with( $value, $emoji_base ) ) {
144 return true;
145 }
146
147 // Only allow remote URLs when caching is explicitly disabled.
148 // This protects user privacy by defaulting to local-only emoji.
149 $allow_remote = ! Cache::is_enabled();
150
151 // Validate the URL format if remote is allowed.
152 if ( $allow_remote ) {
153 $allow_remote = (bool) \wp_http_validate_url( $value );
154 }
155
156 /**
157 * Filters whether a remote emoji URL is valid.
158 *
159 * Use this filter to explicitly allow remote emoji URLs when needed
160 * (e.g., for CDN proxying).
161 *
162 * @since 5.6.0
163 *
164 * @param bool $valid Whether the URL is valid.
165 * @param string $value The emoji src URL.
166 */
167 return \apply_filters( 'activitypub_validate_emoji_src', $allow_remote, $value );
168 }
169
170 /**
171 * Prepare actor meta for emoji storage.
172 *
173 * Used for storing actor emoji data for comment author name rendering.
174 *
175 * @param array $actor The actor array containing potential emoji in tags.
176 *
177 * @return array Meta input array with emoji data, or empty array if no emoji.
178 */
179 public static function prepare_actor_meta( $actor ) {
180 if ( empty( $actor['tag'] ) || ! \is_array( $actor['tag'] ) ) {
181 return array();
182 }
183
184 $emoji_tags = \array_values(
185 \array_filter(
186 $actor['tag'],
187 function ( $tag ) {
188 return \is_array( $tag ) && isset( $tag['type'] ) && 'Emoji' === $tag['type'];
189 }
190 )
191 );
192
193 if ( empty( $emoji_tags ) ) {
194 return array();
195 }
196
197 return array(
198 '_activitypub_emoji' => \wp_json_encode( $emoji_tags ),
199 );
200 }
201
202 /**
203 * Replace emoji from stored JSON data.
204 *
205 * Used for comment author name replacement at display time.
206 *
207 * @param string $text The text to process.
208 * @param string $emoji_json JSON-encoded emoji tag data.
209 *
210 * @return string The processed text with emoji replacements.
211 */
212 public static function replace_from_json( $text, $emoji_json ) {
213 $tags = \json_decode( $emoji_json, true );
214
215 if ( empty( $tags ) || ! \is_array( $tags ) ) {
216 return $text;
217 }
218
219 foreach ( $tags as $tag ) {
220 if ( empty( $tag['name'] ) ) {
221 continue;
222 }
223
224 $url = object_to_uri( $tag['icon'] ?? null );
225 if ( empty( $url ) ) {
226 continue;
227 }
228
229 /**
230 * Filters a remote media URL for caching.
231 *
232 * @param string $url The remote media URL.
233 * @param string $context The context ('emoji').
234 * @param string|null $entity_id The entity ID.
235 * @param array $options Additional options.
236 */
237 $cached_url = \apply_filters(
238 'activitypub_remote_media_url',
239 $url,
240 'emoji',
241 null,
242 array( 'updated' => $tag['updated'] ?? null )
243 );
244
245 $name = \trim( $tag['name'], ':' );
246 $img = self::get_img_tag( $cached_url ?: $url, $name );
247
248 $text = \str_ireplace( $tag['name'], $img, $text );
249 }
250
251 return $text;
252 }
253
254 /**
255 * Replace emoji in text using a remote actor's stored emoji data.
256 *
257 * Used by Mailer class for actor name/summary in emails.
258 *
259 * @param string $text The text to process.
260 * @param string $actor_url The actor's URL to look up emoji data.
261 *
262 * @return string The processed text with emoji replacements.
263 */
264 public static function replace_for_actor( $text, $actor_url ) {
265 $actor_post = Collection\Remote_Actors::get_by_uri( $actor_url );
266 if ( ! $actor_post || \is_wp_error( $actor_post ) ) {
267 return $text;
268 }
269
270 $emoji_data = \get_post_meta( $actor_post->ID, '_activitypub_emoji', true );
271 if ( empty( $emoji_data ) ) {
272 return $text;
273 }
274
275 return self::replace_from_json( $text, $emoji_data );
276 }
277 }
278