PluginProbe
Embed Privacy / trunk
Embed Privacy vtrunk
1.14.0 1.13.0 trunk 0.1 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.10.0 1.10.1 1.10.10 1.10.2 1.10.3 1.10.4 1.10.5 1.10.6 1.10.7 1.10.8 1.10.9 1.11.0 1.11.1 1.11.2 All 68 releases
embed-privacy / inc / integration / class-activitypub.php

class-activitypub.php in Embed Privacy trunk, at inc/integration/class-activitypub.php

218 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace epiphyt\Embed_Privacy\integration;
3
4 use Activitypub\Signature;
5 use DateTimeImmutable;
6 use epiphyt\Embed_Privacy\data\Embed_Cache;
7
8 /**
9 * ActivityPub integration for Embed Privacy.
10 *
11 * @author Epiphyt
12 * @license GPL2
13 * @package epiphyt\Embed_Privacy
14 * @since 1.10.0
15 */
16 final class Activitypub {
17 /**
18 * @var string Local toot template
19 */
20 private static $toot_template = '<blockquote class="embed-privacy__toot">
21 %1$s
22 <cite class="embed-privacy__toot--meta">
23 <span class="embed-privacy__toot--author-meta"><a href="%3$s">%2$s</a></span>
24 <span class="embed-privacy__toot--date-meta"><a href="%5$s">%4$s</a></span>
25 </cite>
26 </blockquote>';
27
28 /**
29 * Initialize functionality.
30 */
31 public static function init() {
32 \add_filter( 'pre_oembed_result', [ self::class, 'maybe_set_cache' ], 9, 2 );
33 \add_filter( 'embed_privacy_custom_oembed_replacement', [ self::class, 'maybe_set_local_toot' ], 10, 4 );
34 \add_filter( 'embed_privacy_is_ignored_request', [ self::class, 'set_ignored_request' ] );
35 }
36
37 /**
38 * Get remote embed data.
39 *
40 * @param string $url URL to retrieve embed data from
41 * @return ?\stdClass JSON object or null
42 */
43 private static function get_remote_embed_data( $url ) {
44 $date = \gmdate( 'D, d M Y H:i:s T' );
45 $headers = [
46 'Accept' => 'application/activity+json',
47 'Content-Type' => 'application/activity+json',
48 'Date' => $date,
49 ];
50
51 if ( \method_exists( '\Activitypub\Signature', 'generate_signature' ) ) {
52 $headers['Signature'] = Signature::generate_signature( -1, 'get', $url, $date );
53 }
54
55 $request = \wp_safe_remote_get(
56 $url,
57 [
58 'headers' => $headers,
59 ]
60 );
61
62 if ( \is_wp_error( $request ) ) {
63 return null;
64 }
65
66 $response = \wp_remote_retrieve_body( $request );
67 $json = \json_decode( $response );
68
69 if ( ! $json ) {
70 return null;
71 }
72
73 return $json;
74 }
75
76 /**
77 * Get a local toot.
78 *
79 * @param \stdClass $data ActivityPub response object
80 * @return string Local toot markup
81 */
82 private static function get_local_toot( $data ) {
83 $date = new DateTimeImmutable( $data->published );
84 $date = $date->setTimezone( \wp_timezone() );
85
86 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
87 return \sprintf(
88 self::$toot_template,
89 \wp_kses_post( $data->content ),
90 \esc_html( self::get_username_from_attributed_to( $data->attributedTo ) ),
91 \esc_url( $data->attributedTo ),
92 $date->format( \get_option( 'date_format' ) . ' ' . \get_option( 'time_format' ) ),
93 \esc_url( $data->url )
94 );
95 // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
96 }
97
98 /**
99 * Extract the username from the 'attributedTo' field.
100 *
101 * @param string $attributed_to Content of the 'attributedTo' field
102 * @return string Username in Webfinger format
103 */
104 private static function get_username_from_attributed_to( $attributed_to ) {
105 $parts = \wp_parse_url( $attributed_to );
106 $username = '';
107
108 if ( isset( $parts['path'] ) ) {
109 if ( \strpos( $parts['path'], '/users/' ) === 0 ) {
110 $username = '@' . \str_replace( '/users/', '', $parts['path'] );
111 }
112 else if ( \strpos( $parts['path'], '/author/' ) === 0 ) {
113 $username = '@' . \str_replace( '/author/', '', $parts['path'] );
114 }
115
116 $username = \rtrim( $username, '/' );
117 }
118
119 if ( isset( $parts['host'] ) ) {
120 $username .= '@' . $parts['host'];
121 }
122
123 return $username;
124 }
125
126 /**
127 * Check, whether the current content is a valid ActivityPub embed.
128 *
129 * @param mixed $content Given content
130 * @return bool Whether the current content is a valid ActivityPub embed
131 */
132 public static function is_valid_embed( $content ) {
133 if ( ! \is_object( $content ) ) {
134 return false;
135 }
136
137 $supported_types = [
138 'Article',
139 'Document',
140 'Event',
141 'Note',
142 'Page',
143 'Tombstone',
144 ];
145
146 $has_context = isset( $content->{'@context'} ) && $content->{'@context'}[0] === 'https://www.w3.org/ns/activitystreams';
147 $is_type = \in_array( $content->type, $supported_types, true );
148
149 return $has_context && $is_type;
150 }
151
152 /**
153 * Maybe set a cache for an ActivityPub embed.
154 * Creates a cache entry only if there is no oEmbed result yet.
155 *
156 * @param ?string $result Current oEmbed result
157 * @param string $url Current oEmbed URL
158 * @return string|false|null oEmbed result
159 */
160 public static function maybe_set_cache( $result, $url ) {
161 if ( ! \get_option( 'embed_privacy_local_activitypub_posts' ) ) {
162 return $result;
163 }
164
165 if ( $result !== null ) {
166 return $result;
167 }
168
169 $cache = Embed_Cache::get( $url );
170
171 if ( $cache === false ) {
172 $cache = self::get_remote_embed_data( $url );
173
174 Embed_Cache::set( $url, $cache );
175 }
176
177 return $result;
178 }
179
180 /**
181 * Set a local toot if it's a valid ActivityPub embed.
182 *
183 * @param string $custom_replacement Current custom replacement
184 * @param string $content The original content
185 * @param \epiphyt\Embed_privacy\embed\Provider $provider Current provider
186 * @param string $url Embed URL
187 * @return string Local toot or original embed
188 */
189 public static function maybe_set_local_toot( $custom_replacement, $content, $provider, $url ) {
190 if ( ! \get_option( 'embed_privacy_local_activitypub_posts' ) ) {
191 return $custom_replacement;
192 }
193
194 $cache = Embed_Cache::get( $url );
195
196 if ( ! self::is_valid_embed( $cache ) ) {
197 return $custom_replacement;
198 }
199
200 return self::get_local_toot( $cache );
201 }
202
203 /**
204 * Set whether the current request is an ActivityPub request and thus should be ignored.
205 * Return the unaltered value if it's already ignored.
206 *
207 * @param bool $is_ignored Whether the current request is ignored
208 * @return bool Whether the current request is ignored
209 */
210 public static function set_ignored_request( $is_ignored ) {
211 if ( $is_ignored ) {
212 return $is_ignored;
213 }
214
215 return \function_exists( 'Activitypub\is_activitypub_request' ) && \Activitypub\is_activitypub_request();
216 }
217 }
218