PluginProbe
ActivityPub / 9.2.2
ActivityPub v9.2.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 / scheduler / class-post.php

class-post.php in ActivityPub 9.2.2, at includes/scheduler/class-post.php

261 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post scheduler class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Scheduler;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Collection\Actors;
12
13 use function Activitypub\add_to_outbox;
14 use function Activitypub\get_post_id;
15 use function Activitypub\get_wp_object_state;
16 use function Activitypub\is_post_disabled;
17 use function Activitypub\is_post_publicly_queryable;
18
19 /**
20 * Post scheduler class.
21 */
22 class Post {
23 /**
24 * Initialize the class, registering WordPress hooks.
25 */
26 public static function init() {
27 // Post transitions.
28 \add_action( 'wp_after_insert_post', array( self::class, 'triage' ), 33, 4 );
29
30 // Attachment transitions.
31 \add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) );
32 \add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) );
33 \add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) );
34
35 /*
36 * Sticky post transitions (featured collection).
37 *
38 * Note: These hooks run in addition to the legacy sticky hooks in
39 * Actor scheduler, which send an Update activity when a post becomes
40 * sticky or is unstuck. This means a sticky/unsticky event will cause both:
41 * - an Add/Remove activity for the Actor's featured collection (below), and
42 * - an Update activity (from Actor scheduler).
43 * The Update activity is kept for backwards compatibility.
44 */
45 \add_action( 'post_stuck', array( self::class, 'schedule_featured_add' ) );
46 \add_action( 'post_unstuck', array( self::class, 'schedule_featured_remove' ) );
47 }
48
49 /**
50 * Triage post transitions and determine the appropriate Activity type.
51 *
52 * @param int $post_id Post ID.
53 * @param \WP_Post $post Post object.
54 * @param bool $update Whether this is an existing post being updated.
55 * @param \WP_Post $post_before Post object before the update.
56 */
57 public static function triage( $post_id, $post, $update, $post_before ) {
58 if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
59 return;
60 }
61
62 if ( is_post_disabled( $post ) ) {
63 return;
64 }
65
66 $object_status = get_wp_object_state( $post );
67 $is_queryable = is_post_publicly_queryable( $post );
68
69 // If the post is already soft-deleted and still non-public, do not create any more activities.
70 if ( ACTIVITYPUB_OBJECT_STATE_DELETED === $object_status && ! $is_queryable ) {
71 return;
72 }
73
74 // Bail on bulk edits, unless post author or post status changed.
75 if ( isset( $_REQUEST['bulk_edit'] ) && ( ! isset( $_REQUEST['post_author'] ) || -1 === (int) $_REQUEST['post_author'] ) && -1 === (int) $_REQUEST['_status'] ) { // phpcs:ignore WordPress
76 return;
77 }
78
79 $new_status = \get_post_status( $post );
80 $old_status = $post_before ? \get_post_status( $post_before ) : null;
81
82 switch ( $new_status ) {
83 case 'publish':
84 if ( $update ) {
85 $type = ( 'publish' === $old_status ) ? 'Update' : 'Create';
86 } else {
87 $type = 'Create';
88 }
89 break;
90
91 case 'future':
92 /*
93 * A (re-)scheduled post is not a deletion: it becomes public again
94 * when it publishes, at which point the publish transition federates
95 * it. Treating `future` as a soft delete would fan out a Delete that
96 * remotely tombstones the object id — e.g. when a content edit reverts
97 * a future-dated published post back to `future` — after which the
98 * post can never re-federate. Emit nothing instead.
99 */
100 $type = false;
101 break;
102
103 case 'draft':
104 case 'pending':
105 case 'private':
106 case 'trash':
107 default:
108 /*
109 * Soft delete for federated posts (FEP-4f05).
110 *
111 * A previously-federated post transitioning to any non-public
112 * status (built-in or custom) emits a Delete so federated
113 * copies are torn down. Without this, draft/pending would
114 * broadcast a placeholder Update, private/trash would silently
115 * leave the federated copy stale, and a custom status would
116 * fall through without notifying followers at all.
117 */
118 $type = ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && ! $is_queryable
119 ? 'Delete'
120 : false;
121 }
122
123 // Do not send Activities if `$type` is not set or unknown.
124 if ( empty( $type ) ) {
125 return;
126 }
127
128 /*
129 * If the post was already federated and this is a Create, skip.
130 * The outbox controller already added it to the outbox.
131 */
132 if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && 'Create' === $type ) {
133 return;
134 }
135
136 // If the post was never federated before, it should be a Create activity.
137 if ( empty( $object_status ) && 'Update' === $type ) {
138 $type = 'Create';
139 }
140
141 /*
142 * Resurrection: a soft-deleted post that is back in a publicly
143 * queryable state must emit Create, not Update. Remote followers
144 * either dropped the original Create on the Delete fan-out (so
145 * they need to learn about the post again) or had it cancelled
146 * before fanning out (so the supersession logic invalidates the
147 * pending Delete and Create is the correct re-introduction).
148 */
149 if ( ACTIVITYPUB_OBJECT_STATE_DELETED === $object_status && 'Update' === $type && $is_queryable ) {
150 $type = 'Create';
151 }
152
153 // If the post was federated before but is now non-public, it should be a Delete activity.
154 if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && ! $is_queryable ) {
155 $type = 'Delete';
156 }
157
158 add_to_outbox( $post, $type, $post->post_author );
159 }
160
161 /**
162 * Schedules Activities for attachment transitions.
163 *
164 * @param int $post_id Attachment ID.
165 */
166 public static function transition_attachment_status( $post_id ) {
167 if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
168 return;
169 }
170
171 if ( ! \post_type_supports( 'attachment', 'activitypub' ) ) {
172 return;
173 }
174
175 if ( is_post_disabled( $post_id ) ) {
176 return;
177 }
178
179 $post = \get_post( $post_id );
180
181 if ( ! $post instanceof \WP_Post ) {
182 return;
183 }
184
185 switch ( \current_action() ) {
186 case 'add_attachment':
187 $type = 'Create';
188 break;
189 case 'edit_attachment':
190 $type = 'Update';
191 break;
192 case 'delete_attachment':
193 $type = 'Delete';
194 break;
195 default:
196 return;
197 }
198
199 add_to_outbox( $post, $type, $post->post_author );
200 }
201
202 /**
203 * Schedule an Add activity when a post is added to the featured collection.
204 *
205 * @param int $post_id The post ID.
206 */
207 public static function schedule_featured_add( $post_id ) {
208 self::schedule_featured_update( $post_id, 'Add' );
209 }
210
211 /**
212 * Schedule a Remove activity when a post is removed from the featured collection.
213 *
214 * @param int $post_id The post ID.
215 */
216 public static function schedule_featured_remove( $post_id ) {
217 self::schedule_featured_update( $post_id, 'Remove' );
218 }
219
220 /**
221 * Schedule an Add or Remove activity for the featured collection.
222 *
223 * When a post's sticky status changes, this sends an Add or Remove activity
224 * to notify followers about the change to the actor's featured collection.
225 *
226 * @see https://github.com/Automattic/wordpress-activitypub/issues/2795
227 *
228 * @param int $post_id The post ID.
229 * @param string $activity_type The activity type ('Add' or 'Remove').
230 */
231 private static function schedule_featured_update( $post_id, $activity_type ) {
232 if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
233 return;
234 }
235
236 $post = \get_post( $post_id );
237
238 if ( ! $post ) {
239 return;
240 }
241
242 if ( is_post_disabled( $post ) ) {
243 return;
244 }
245
246 $actor = Actors::get_by_id( $post->post_author );
247
248 if ( ! $actor || \is_wp_error( $actor ) ) {
249 return;
250 }
251
252 $activity = new Activity();
253 $activity->set_type( $activity_type );
254 $activity->set_actor( $actor->get_id() );
255 $activity->set_object( get_post_id( $post->ID ) );
256 $activity->set_target( $actor->get_featured() );
257
258 add_to_outbox( $activity, null, $post->post_author );
259 }
260 }
261