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 / integration / class-classic-editor.php

class-classic-editor.php in ActivityPub trunk, at integration/class-classic-editor.php

318 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Classic Editor integration file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Integration;
9
10 /**
11 * Classic Editor integration class.
12 *
13 * Handles compatibility with the Classic Editor plugin and sites without block editor support.
14 */
15 class Classic_Editor {
16
17 /**
18 * Initialize the class, registering WordPress hooks.
19 */
20 public static function init() {
21 \add_filter( 'activitypub_attachments_media_markup', array( self::class, 'filter_attachments_media_markup' ), 10, 2 );
22 \add_filter( 'activitypub_attachment_ids', array( self::class, 'filter_attached_media_ids' ), 10, 2 );
23 \add_action( 'add_meta_boxes', array( self::class, 'add_meta_box' ), 10, 2 );
24 \add_action( 'save_post', array( self::class, 'save_meta_data' ) );
25
26 if ( \function_exists( 'classicpress_version' ) ) {
27 \add_filter( 'activitypub_site_supports_blocks', '__return_false' );
28 }
29 }
30
31 /**
32 * Filter attachment media markup to use shortcodes instead of blocks.
33 *
34 * @param string $markup The custom markup. Empty string by default.
35 * @param array $attachment_ids Array of attachment IDs.
36 *
37 * @return string The generated shortcode markup.
38 */
39 public static function filter_attachments_media_markup( $markup, $attachment_ids ) {
40 if ( empty( $attachment_ids ) ) {
41 return $markup;
42 }
43
44 $type = \strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
45
46 // Single video or audio file: use media shortcode.
47 if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) {
48 return \sprintf(
49 '[%1$s src="%2$s"]',
50 \esc_attr( $type ),
51 \esc_url( \wp_get_attachment_url( $attachment_ids[0] ) )
52 );
53 }
54
55 // Multiple attachments or images: use gallery shortcode.
56 return '[gallery ids="' . \implode( ',', $attachment_ids ) . '" link="none"]';
57 }
58
59 /**
60 * Filter to add attached media IDs from the post's media library.
61 *
62 * Returns additional image attachments from the post's media library,
63 * respecting the maximum image attachment limit. Only returns new
64 * attachments that can be added without exceeding the limit.
65 *
66 * @param array $attachments The current list of attachments.
67 * @param \WP_Post $item The post item.
68 *
69 * @return array Array of new media IDs to add (as associative arrays with 'id' key).
70 * Returns empty array if already at or over the limit.
71 */
72 public static function filter_attached_media_ids( $attachments, $item ) {
73 $max_media = \get_option( 'activitypub_max_image_attachments', \ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS );
74 $actual_count = \max( 0, $max_media - \count( $attachments ) );
75
76 if ( $actual_count <= 0 ) {
77 return $attachments;
78 }
79
80 $query = new \WP_Query(
81 array(
82 'post_parent' => $item->ID,
83 'post_status' => 'inherit',
84 'post_type' => 'attachment',
85 'post_mime_type' => 'image',
86 'order' => 'ASC',
87 'orderby' => 'menu_order ID',
88 'fields' => 'ids',
89 'posts_per_page' => $actual_count,
90 )
91 );
92
93 // Transform IDs into associative arrays.
94 $media_ids = \array_map(
95 static function ( $id ) {
96 return array( 'id' => $id );
97 },
98 $query->get_posts()
99 );
100
101 return \array_merge( $attachments, $media_ids );
102 }
103
104 /**
105 * Add ActivityPub meta box to the post editor.
106 *
107 * @param string $post_type The post type.
108 * @param \WP_Post|null $post The post being edited.
109 */
110 public static function add_meta_box( $post_type, $post = null ) {
111 // Only add for post types that support ActivityPub.
112 if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
113 return;
114 }
115
116 /*
117 * The block editor ships its own ActivityPub panel, so the classic meta box must not be
118 * added there. With the Classic Editor plugin in switchable mode a post can still open in
119 * the block editor, where both would otherwise render and the meta box's save_post handler
120 * would overwrite the panel's value. The function_exists() check keeps ClassicPress and
121 * other block-less setups (where use_block_editor_for_post() is absent) safe.
122 */
123 if ( $post instanceof \WP_Post && \function_exists( 'use_block_editor_for_post' ) && \use_block_editor_for_post( $post ) ) {
124 return;
125 }
126
127 \add_meta_box(
128 'activitypub-settings',
129 \__( 'Fediverse ⁂', 'activitypub' ),
130 array( self::class, 'render_meta_box' ),
131 $post_type,
132 'side',
133 'default'
134 );
135 }
136
137 /**
138 * Render the ActivityPub meta box.
139 *
140 * @param \WP_Post $post The post object.
141 */
142 public static function render_meta_box( $post ) {
143 // Add nonce for security.
144 \wp_nonce_field( 'activitypub_meta_box', 'activitypub_meta_box_nonce' );
145
146 // Get current values.
147 $content_warning = \get_post_meta( $post->ID, 'activitypub_content_warning', true );
148 $max_image_attachments = \get_post_meta( $post->ID, 'activitypub_max_image_attachments', true );
149 $content_visibility = self::get_default_visibility( $post );
150 $default_quote_policy = \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE );
151 $quote_interaction = \get_post_meta( $post->ID, 'activitypub_interaction_policy_quote', true ) ?: $default_quote_policy;
152
153 ?>
154 <p>
155 <label for="activitypub_content_warning">
156 <strong><?php \esc_html_e( 'Content Warning', 'activitypub' ); ?></strong>
157 </label><br />
158 <input type="text" id="activitypub_content_warning" name="activitypub_content_warning" value="<?php echo \esc_attr( $content_warning ); ?>" class="widefat" placeholder="<?php \esc_attr_e( 'Optional content warning', 'activitypub' ); ?>" />
159 <span class="howto"><?php \esc_html_e( 'Content warnings do not change the content on your site, only in the fediverse.', 'activitypub' ); ?></span>
160 </p>
161
162 <p>
163 <label for="activitypub_max_image_attachments">
164 <strong><?php \esc_html_e( 'Maximum Image Attachments', 'activitypub' ); ?></strong>
165 </label><br />
166 <input type="number" id="activitypub_max_image_attachments" name="activitypub_max_image_attachments" value="<?php echo \esc_attr( $max_image_attachments ); ?>" min="0" max="10" class="small-text" />
167 <span class="howto"><?php \esc_html_e( 'Maximum number of image attachments to include when sharing to the fediverse.', 'activitypub' ); ?></span>
168 </p>
169
170 <p>
171 <strong><?php \esc_html_e( 'Visibility', 'activitypub' ); ?></strong><br />
172 <label>
173 <input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); ?> />
174 <?php \esc_html_e( 'Public', 'activitypub' ); ?>
175 </label><br />
176 <label>
177 <input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); ?> />
178 <?php \esc_html_e( 'Quiet public', 'activitypub' ); ?>
179 </label><br />
180 <label>
181 <input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ); ?> />
182 <?php \esc_html_e( 'Do not federate', 'activitypub' ); ?>
183 </label><br />
184 <span class="howto"><?php \esc_html_e( 'This adjusts the visibility of a post in the fediverse, but note that it won\'t affect how the post appears on the blog.', 'activitypub' ); ?></span>
185 </p>
186
187 <p>
188 <label for="activitypub_interaction_policy_quote">
189 <strong><?php \esc_html_e( 'Who can quote this post?', 'activitypub' ); ?></strong>
190 </label><br />
191 <select id="activitypub_interaction_policy_quote" name="activitypub_interaction_policy_quote" class="widefat">
192 <option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_ANYONE ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_ANYONE ); ?>><?php \esc_html_e( 'Anyone', 'activitypub' ); ?></option>
193 <option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS ); ?>><?php \esc_html_e( 'Followers only', 'activitypub' ); ?></option>
194 <option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_ME ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_ME ); ?>><?php \esc_html_e( 'Just me', 'activitypub' ); ?></option>
195 </select>
196 <span class="howto">
197 <?php \esc_html_e( 'Quoting allows others to cite your post while adding their own commentary.', 'activitypub' ); ?>
198 <?php
199 \printf(
200 /* translators: %s: The current site default quote policy. Note the leading space. */
201 \esc_html__( ' Site default: %s', 'activitypub' ),
202 \esc_html( self::get_quote_policy_label( $default_quote_policy ) )
203 );
204 ?>
205 </span>
206 </p>
207 <?php
208 }
209
210 /**
211 * Get the label for a quote policy value.
212 *
213 * @param string $policy The policy value.
214 *
215 * @return string The translated label.
216 */
217 private static function get_quote_policy_label( $policy ) {
218 $labels = array(
219 ACTIVITYPUB_INTERACTION_POLICY_ANYONE => \__( 'Anyone', 'activitypub' ),
220 ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS => \__( 'Followers only', 'activitypub' ),
221 ACTIVITYPUB_INTERACTION_POLICY_ME => \__( 'Just me', 'activitypub' ),
222 );
223
224 return $labels[ $policy ] ?? $labels[ ACTIVITYPUB_INTERACTION_POLICY_ANYONE ];
225 }
226
227 /**
228 * Get default visibility based on post age and federation status.
229 *
230 * @param \WP_Post $post The post object.
231 *
232 * @return string The default visibility value.
233 */
234 private static function get_default_visibility( $post ) {
235 // If already set, use that value.
236 $saved_visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
237 if ( $saved_visibility ) {
238 return $saved_visibility;
239 }
240
241 // If post is federated, use public.
242 $status = \get_post_meta( $post->ID, 'activitypub_status', true );
243 if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $status ) {
244 return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
245 }
246
247 // If post is older than 1 month, default to local.
248 $post_timestamp = \strtotime( $post->post_date );
249 $one_month_ago = \strtotime( '-30 days' );
250
251 if ( $post_timestamp < $one_month_ago ) {
252 return ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL;
253 }
254
255 // Default to public for new posts.
256 return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
257 }
258
259 /**
260 * Save ActivityPub meta data.
261 *
262 * @param int $post_id The post ID.
263 */
264 public static function save_meta_data( $post_id ) {
265 // Check if this is an autosave.
266 if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
267 return;
268 }
269
270 // Only process for post types that support ActivityPub.
271 if ( ! \post_type_supports( \get_post_type( $post_id ), 'activitypub' ) ) {
272 return;
273 }
274
275 // Check user permissions.
276 if ( ! \current_user_can( 'edit_post', $post_id ) ) {
277 return;
278 }
279
280 // Verify nonce is present and valid.
281 if ( ! isset( $_POST['activitypub_meta_box_nonce'] ) ) {
282 return;
283 }
284
285 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['activitypub_meta_box_nonce'] ) ), 'activitypub_meta_box' ) ) {
286 return;
287 }
288
289 // Save content warning.
290 if ( isset( $_POST['activitypub_content_warning'] ) ) {
291 $content_warning = \sanitize_text_field( \wp_unslash( $_POST['activitypub_content_warning'] ) );
292 if ( ! empty( $content_warning ) ) {
293 \update_post_meta( $post_id, 'activitypub_content_warning', $content_warning );
294 } else {
295 \delete_post_meta( $post_id, 'activitypub_content_warning' );
296 }
297 }
298
299 // Save max image attachments.
300 if ( isset( $_POST['activitypub_max_image_attachments'] ) ) {
301 $max_images = \absint( $_POST['activitypub_max_image_attachments'] );
302 \update_post_meta( $post_id, 'activitypub_max_image_attachments', $max_images );
303 }
304
305 // Save content visibility.
306 if ( isset( $_POST['activitypub_content_visibility'] ) ) {
307 $visibility = \sanitize_text_field( \wp_unslash( $_POST['activitypub_content_visibility'] ) );
308 \update_post_meta( $post_id, 'activitypub_content_visibility', $visibility );
309 }
310
311 // Save quote interaction policy.
312 if ( isset( $_POST['activitypub_interaction_policy_quote'] ) ) {
313 $quote_policy = \sanitize_text_field( \wp_unslash( $_POST['activitypub_interaction_policy_quote'] ) );
314 \update_post_meta( $post_id, 'activitypub_interaction_policy_quote', $quote_policy );
315 }
316 }
317 }
318