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 / scheduler / class-post.php

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

249 lines 7.1 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 'draft':
92 case 'pending':
93 case 'private':
94 case 'trash':
95 default:
96 /*
97 * Soft delete for federated posts (FEP-4f05).
98 *
99 * A previously-federated post transitioning to any non-public
100 * status (built-in or custom) emits a Delete so federated
101 * copies are torn down. Without this, draft/pending would
102 * broadcast a placeholder Update, private/trash would silently
103 * leave the federated copy stale, and a custom status would
104 * fall through without notifying followers at all.
105 */
106 $type = ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && ! $is_queryable
107 ? 'Delete'
108 : false;
109 }
110
111 // Do not send Activities if `$type` is not set or unknown.
112 if ( empty( $type ) ) {
113 return;
114 }
115
116 /*
117 * If the post was already federated and this is a Create, skip.
118 * The outbox controller already added it to the outbox.
119 */
120 if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && 'Create' === $type ) {
121 return;
122 }
123
124 // If the post was never federated before, it should be a Create activity.
125 if ( empty( $object_status ) && 'Update' === $type ) {
126 $type = 'Create';
127 }
128
129 /*
130 * Resurrection: a soft-deleted post that is back in a publicly
131 * queryable state must emit Create, not Update. Remote followers
132 * either dropped the original Create on the Delete fan-out (so
133 * they need to learn about the post again) or had it cancelled
134 * before fanning out (so the supersession logic invalidates the
135 * pending Delete and Create is the correct re-introduction).
136 */
137 if ( ACTIVITYPUB_OBJECT_STATE_DELETED === $object_status && 'Update' === $type && $is_queryable ) {
138 $type = 'Create';
139 }
140
141 // If the post was federated before but is now non-public, it should be a Delete activity.
142 if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $object_status && ! $is_queryable ) {
143 $type = 'Delete';
144 }
145
146 add_to_outbox( $post, $type, $post->post_author );
147 }
148
149 /**
150 * Schedules Activities for attachment transitions.
151 *
152 * @param int $post_id Attachment ID.
153 */
154 public static function transition_attachment_status( $post_id ) {
155 if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
156 return;
157 }
158
159 if ( ! \post_type_supports( 'attachment', 'activitypub' ) ) {
160 return;
161 }
162
163 if ( is_post_disabled( $post_id ) ) {
164 return;
165 }
166
167 $post = \get_post( $post_id );
168
169 if ( ! $post instanceof \WP_Post ) {
170 return;
171 }
172
173 switch ( \current_action() ) {
174 case 'add_attachment':
175 $type = 'Create';
176 break;
177 case 'edit_attachment':
178 $type = 'Update';
179 break;
180 case 'delete_attachment':
181 $type = 'Delete';
182 break;
183 default:
184 return;
185 }
186
187 add_to_outbox( $post, $type, $post->post_author );
188 }
189
190 /**
191 * Schedule an Add activity when a post is added to the featured collection.
192 *
193 * @param int $post_id The post ID.
194 */
195 public static function schedule_featured_add( $post_id ) {
196 self::schedule_featured_update( $post_id, 'Add' );
197 }
198
199 /**
200 * Schedule a Remove activity when a post is removed from the featured collection.
201 *
202 * @param int $post_id The post ID.
203 */
204 public static function schedule_featured_remove( $post_id ) {
205 self::schedule_featured_update( $post_id, 'Remove' );
206 }
207
208 /**
209 * Schedule an Add or Remove activity for the featured collection.
210 *
211 * When a post's sticky status changes, this sends an Add or Remove activity
212 * to notify followers about the change to the actor's featured collection.
213 *
214 * @see https://github.com/Automattic/wordpress-activitypub/issues/2795
215 *
216 * @param int $post_id The post ID.
217 * @param string $activity_type The activity type ('Add' or 'Remove').
218 */
219 private static function schedule_featured_update( $post_id, $activity_type ) {
220 if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
221 return;
222 }
223
224 $post = \get_post( $post_id );
225
226 if ( ! $post ) {
227 return;
228 }
229
230 if ( is_post_disabled( $post ) ) {
231 return;
232 }
233
234 $actor = Actors::get_by_id( $post->post_author );
235
236 if ( ! $actor || \is_wp_error( $actor ) ) {
237 return;
238 }
239
240 $activity = new Activity();
241 $activity->set_type( $activity_type );
242 $activity->set_actor( $actor->get_id() );
243 $activity->set_object( get_post_id( $post->ID ) );
244 $activity->set_target( $actor->get_featured() );
245
246 add_to_outbox( $activity, null, $post->post_author );
247 }
248 }
249