PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | _inc/blogging-prompts.php +378 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,378 @@
1 +<?php
2 +/**
3 + * Used by the blogging prompt feature.
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +if ( ! defined( 'ABSPATH' ) ) {
9 + exit( 0 );
10 +}
11 +
12 +/**
13 + * Hooked functions.
14 + */
15 +
16 +/**
17 + * Adds the blogging prompt key post meta to the list of allowed post meta to be updated by rest api.
18 + *
19 + * @param array $keys Array of post meta keys that are allowed public metadata.
20 + *
21 + * @return array
22 + */
23 +function jetpack_blogging_prompts_add_meta_data( $keys ) {
24 + $keys[] = '_jetpack_blogging_prompt_key';
25 + return $keys;
26 +}
27 +
28 +add_filter( 'rest_api_allowed_public_metadata', 'jetpack_blogging_prompts_add_meta_data' );
29 +
30 +/**
31 + * Sets up a new post as an answer to a blogging prompt (classic new-post screen).
32 + *
33 + * When we know a user is explicitly answering a prompt, pre-populate the post meta to mark the post as a prompt response,
34 + * in case they decide to remove the block from the post content, preventing they meta from being added later.
35 + *
36 + * REST creations (e.g. the Write editor's POST /wp/v2/posts?answer_prompt=…) are
37 + * handled by jetpack_setup_blogging_prompt_response_rest() on rest_after_insert_post
38 + * instead — that hook runs after the REST controller sets the request's tags, so the
39 + * prompt tags we add aren't overwritten.
40 + *
41 + * Called on `wp_insert_post` hook.
42 + *
43 + * @param int $post_id ID of post being inserted.
44 + * @return void
45 + */
46 +function jetpack_setup_blogging_prompt_response( $post_id ) {
47 + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
48 + return;
49 + }
50 +
51 + if ( ! jetpack_is_new_post_screen() ) {
52 + return;
53 + }
54 +
55 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Clicking a prompt response link can happen from notifications, Calypso, wp-admin, email, etc and only sets up a response post (tag, meta, prompt text); the user must take action to actually publish the post.
56 + $prompt_id = isset( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : 0;
57 + if ( $prompt_id ) {
58 + jetpack_apply_blogging_prompt_response( $post_id, $prompt_id );
59 + }
60 +}
61 +
62 +add_action( 'wp_insert_post', 'jetpack_setup_blogging_prompt_response' );
63 +
64 +/**
65 + * Sets up a REST-created post (e.g. the Write editor) as an answer to a prompt.
66 + *
67 + * Runs on `rest_after_insert_post`, which fires after the REST controller has set
68 + * the request's tags — so the prompt tags added here survive.
69 + *
70 + * @param WP_Post $post Inserted post object.
71 + * @param WP_REST_Request $request Request object.
72 + * @param bool $creating True when creating, false when updating.
73 + * @return void
74 + */
75 +function jetpack_setup_blogging_prompt_response_rest( $post, $request, $creating ) {
76 + if ( ! $creating || ! $post instanceof WP_Post || 'post' !== $post->post_type ) {
77 + return;
78 + }
79 +
80 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET param forwarded by the Write editor on the create request; only sets up a response post (tag, meta).
81 + $prompt_id = isset( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : 0;
82 + if ( $prompt_id ) {
83 + jetpack_apply_blogging_prompt_response( $post->ID, $prompt_id );
84 + }
85 +}
86 +
87 +add_action( 'rest_after_insert_post', 'jetpack_setup_blogging_prompt_response_rest', 10, 3 );
88 +
89 +/**
90 + * Stamp a post as a blogging-prompt answer: prompt-key meta + prompt tags.
91 + *
92 + * Shared by the classic (wp_insert_post) and REST (rest_after_insert_post) entry
93 + * points so both mark the answer identically.
94 + *
95 + * @param int $post_id Post ID.
96 + * @param int $prompt_id Prompt ID.
97 + * @return void
98 + */
99 +function jetpack_apply_blogging_prompt_response( $post_id, $prompt_id ) {
100 + // Make sure the prompt exists.
101 + $prompt = jetpack_get_blogging_prompt_by_id( $prompt_id );
102 +
103 + if ( ! $prompt ) {
104 + return;
105 + }
106 +
107 + update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id );
108 + wp_add_post_tags( $post_id, array( 'dailyprompt', "dailyprompt-$prompt_id" ) );
109 + if ( is_array( $prompt ) && array_key_exists( 'bloganuary_id', $prompt ) ) {
110 + wp_add_post_tags( $post_id, array( 'bloganuary', $prompt['bloganuary_id'] ) );
111 + }
112 +}
113 +
114 +/**
115 + * When a published posts answers a blogging prompt, store the prompt id in the post meta.
116 + *
117 + * @param int $post_id Post ID.
118 + * @param WP_Post $post Post object.
119 + * @param bool $update Whether this is an existing post being updated.
120 + * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
121 + * to the update for updated posts.
122 + */
123 +function jetpack_mark_if_post_answers_blogging_prompt( $post_id, $post, $update, $post_before ) {
124 + if ( ! $post instanceof WP_Post ) {
125 + return;
126 + }
127 +
128 + $post_type = $post->post_type ?? null;
129 + $post_content = $post->post_content ?? null;
130 +
131 + if ( 'post' !== $post_type || ! $post_content ) {
132 + return;
133 + }
134 +
135 + $new_status = $post->post_status ?? null;
136 + $old_status = $post_before && isset( $post_before->post_status ) ? $post_before->post_status : null;
137 +
138 + // Make sure we are publishing a post, and it's not already published.
139 + if ( 'publish' !== $new_status || 'publish' === $old_status ) {
140 + return;
141 + }
142 +
143 + $scanner = \Automattic\Block_Scanner::create( $post->post_content );
144 + if ( ! $scanner ) {
145 + return;
146 + }
147 +
148 + $prompt_id = null;
149 + $total_blocks = 0;
150 + $found_prompt_block = false;
151 +
152 + while ( $scanner->next_delimiter() ) {
153 + if ( $scanner->opens_block() ) {
154 + ++$total_blocks;
155 +
156 + if ( ! $found_prompt_block && $scanner->is_block_type( 'jetpack/blogging-prompt' ) ) {
157 + $attributes = $scanner->allocate_and_return_parsed_attributes();
158 + if ( $attributes && isset( $attributes['promptId'] ) ) {
159 + $prompt_id = absint( $attributes['promptId'] );
160 + }
161 + $found_prompt_block = true;
162 + }
163 +
164 + // Early exit: if we found the prompt and have >1 blocks, we have all info needed
165 + if ( $found_prompt_block && $total_blocks > 1 ) {
166 + break;
167 + }
168 + }
169 + }
170 +
171 + if ( ! $found_prompt_block || ! $prompt_id || $total_blocks <= 1 ) {
172 + return;
173 + }
174 +
175 + $has_prompt_tag = has_tag( 'dailyprompt', $post ) || has_tag( "dailyprompt-{$prompt_id}", $post );
176 +
177 + if ( ! $has_prompt_tag ) {
178 + return;
179 + }
180 +
181 + update_post_meta( $post->ID, '_jetpack_blogging_prompt_key', $prompt_id );
182 +}
183 +
184 +add_action( 'wp_after_insert_post', 'jetpack_mark_if_post_answers_blogging_prompt', 10, 4 );
185 +
186 +/**
187 + * Utility functions.
188 + */
189 +
190 +/**
191 + * Build the blogging-prompts route for the REST context we're running in.
192 + *
193 + * The endpoint sets `wpcom_is_site_specific_endpoint`, so WordPress.com's
194 + * centralized REST API registers it site-scoped, as
195 + * `/wpcom/v3/sites/<blog_id>/blogging-prompts/<id>`. Everywhere else — Atomic,
196 + * self-hosted, and ordinary wp-admin requests on Simple — it registers at
197 + * `/wpcom/v3/blogging-prompts/<id>`. Which shape is live depends on the request
198 + * we happen to be running inside, and asking for the wrong one just 404s and
199 + * silently loses the prompt, so resolve it against the route table each time.
200 + *
201 + * Call this only after the endpoint file is required, so its routes are in
202 + * place by the time `rest_get_server()` fires `rest_api_init`.
203 + *
204 + * @since 16.2
205 + *
206 + * @param int $prompt_id ID of the prompt to fetch.
207 + * @return string REST route for that prompt.
208 + */
209 +function jetpack_get_blogging_prompt_route( $prompt_id ) {
210 + foreach ( array_keys( rest_get_server()->get_routes() ) as $route ) {
211 + if ( str_starts_with( $route, '/wpcom/v3/blogging-prompts/' ) ) {
212 + return sprintf( '/wpcom/v3/blogging-prompts/%d', $prompt_id );
213 + }
214 + }
215 +
216 + return sprintf( '/wpcom/v3/sites/%d/blogging-prompts/%d', get_current_blog_id(), $prompt_id );
217 +}
218 +
219 +/**
220 + * Retrieve a blogging prompt by prompt ID.
221 + *
222 + * @param int $prompt_id ID of the prompt fetch.
223 + * @return array|null Prompt object or null.
224 + */
225 +function jetpack_get_blogging_prompt_by_id( $prompt_id ) {
226 + // Ensure the REST API endpoint we need is loaded.
227 + require_once __DIR__ . '/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php';
228 +
229 + $locale = get_locale();
230 + $route = jetpack_get_blogging_prompt_route( $prompt_id );
231 +
232 + $request = new WP_REST_Request( 'GET', $route );
233 + $request->set_param( '_locale', $locale );
234 + $request->set_param( 'force_year', gmdate( 'Y' ) );
235 +
236 + $response = rest_do_request( $request );
237 +
238 + if ( $response->is_error() || WP_Http::OK !== $response->get_status() ) {
239 + return null;
240 + }
241 +
242 + $prompt = $response->get_data();
243 +
244 + return $prompt;
245 +}
246 +
247 +/**
248 + * Retrieve daily blogging prompts from the wpcom API and cache them.
249 + *
250 + * @param int $time Unix timestamp representing the day for which to get blogging prompts.
251 + * @return stdClass[]|null Array of blogging prompt objects or null.
252 + */
253 +function jetpack_get_daily_blogging_prompts( $time = 0 ) {
254 + $timestamp = $time ? $time : time();
255 +
256 + // Include prompts from the previous day, just in case someone has an outdated prompt id.
257 + $day_before = wp_date( 'Y-m-d', $timestamp - DAY_IN_SECONDS );
258 + $locale = get_locale();
259 + $transient_key = 'jetpack_blogging_prompt_' . $day_before . '_' . $locale;
260 + $daily_prompts = get_transient( $transient_key );
261 +
262 + // Return the cached prompt, if we have it. Otherwise fetch it from the API.
263 + if ( false !== $daily_prompts ) {
264 + return $daily_prompts;
265 + }
266 +
267 + $blog_id = \Jetpack_Options::get_option( 'id' );
268 + $path = '/sites/' . rawurldecode( $blog_id ) . '/blogging-prompts?from=' . rawurldecode( $day_before ) . '&number=10&_locale=' . rawurldecode( $locale );
269 +
270 + $args = array(
271 + 'headers' => array(
272 + 'Content-Type' => 'application/json',
273 + 'X-Forwarded-For' => ( new \Automattic\Jetpack\Status\Visitor() )->get_ip( true ),
274 + ),
275 + // `method` and `url` are needed for using `WPCOM_API_Direct::do_request`
276 + // `wpcom_json_api_request_as_user` will generate and overwrite these.
277 + 'method' => \WP_REST_Server::READABLE,
278 + 'url' => JETPACK__WPCOM_JSON_API_BASE . '/wpcom/v2' . $path,
279 + );
280 +
281 + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
282 + // This will load the library, but it may be too late to automatically load any endpoints using WPCOM_API_Direct::register_endpoints.
283 + // In that case, call `wpcom_rest_api_v2_load_plugin_files( 'wp-content/rest-api-plugins/endpoints/blogging-prompts.php' )`
284 + // on the `init` hook to load the blogging-prompts endpoint before calling this function.
285 + require_once WP_CONTENT_DIR . '/lib/wpcom-api-direct/wpcom-api-direct.php';
286 + $response = \WPCOM_API_Direct::do_request( $args );
287 + } else {
288 + $response = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user( $path, 'v2', $args, null, 'wpcom' );
289 + }
290 + $response_status = wp_remote_retrieve_response_code( $response );
291 +
292 + if ( is_wp_error( $response ) || $response_status !== \WP_Http::OK ) {
293 + return null;
294 + }
295 +
296 + $body = json_decode( wp_remote_retrieve_body( $response ) );
297 +
298 + if ( ! $body || ! isset( $body->prompts ) ) {
299 + return null;
300 + }
301 +
302 + $prompts = $body->prompts;
303 + set_transient( $transient_key, $prompts, DAY_IN_SECONDS );
304 +
305 + return $prompts;
306 +}
307 +
308 +/**
309 + * Determines if the site has publish posts or plans to publish posts.
310 + *
311 + * @return bool
312 + */
313 +function jetpack_has_or_will_publish_posts() {
314 + // Lets count the posts.
315 + $count_posts_object = wp_count_posts( 'post' );
316 + $count_posts = (int) $count_posts_object->publish + (int) $count_posts_object->future + (int) $count_posts_object->draft;
317 +
318 + return $count_posts_object->publish >= 2 || $count_posts >= 100;
319 +}
320 +
321 +/**
322 + * Determines if the site has a posts page or shows posts on the front page.
323 + *
324 + * @return bool
325 + */
326 +function jetpack_has_posts_page() {
327 + // The site is set up to be a blog.
328 + if ( 'posts' === get_option( 'show_on_front' ) ) {
329 + return true;
330 + }
331 +
332 + // There is a page set to show posts.
333 + $is_posts_page_set = (int) get_option( 'page_for_posts' ) > 0;
334 + if ( $is_posts_page_set ) {
335 + return true;
336 + }
337 +
338 + return false;
339 +}
340 +
341 +/**
342 + * Determines if site had the "Write" intent set when created.
343 + *
344 + * @return bool
345 + */
346 +function jetpack_has_write_intent() {
347 + return 'write' === get_option( 'site_intent', '' );
348 +}
349 +
350 +/**
351 + * Determines if the current screen (in wp-admin) is creating a new post.
352 + *
353 + * /wp-admin/post-new.php
354 + *
355 + * @return bool
356 + */
357 +function jetpack_is_new_post_screen() {
358 + global $current_screen;
359 +
360 + if (
361 + $current_screen instanceof \WP_Screen &&
362 + 'add' === $current_screen->action &&
363 + 'post' === $current_screen->post_type
364 + ) {
365 + return true;
366 + }
367 +
368 + return false;
369 +}
370 +
371 +/**
372 + * Determines if the site might have a blog.
373 + *
374 + * @return bool
375 + */
376 +function jetpack_is_potential_blogging_site() {
377 + return jetpack_has_write_intent() || jetpack_has_posts_page() || jetpack_has_or_will_publish_posts();
378 +}