| @@ -4,8 +4,12 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * @package automattic/jetpack |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 9 | + exit( 0 ); | |
| 10 | +} | |
| 11 | + | |
| 8 | 12 | /** |
| 9 | 13 | * Hooked functions. |
| 10 | 14 | */ |
| 11 | 15 | |
| @@ -23,13 +27,18 @@ | ||
| 23 | 27 | |
| 24 | 28 | add_filter( 'rest_api_allowed_public_metadata', 'jetpack_blogging_prompts_add_meta_data' ); |
| 25 | 29 | |
| 26 | 30 | /** |
| 27 | - * Sets up a new post as an answer to a blogging prompt. | |
| 31 | + * Sets up a new post as an answer to a blogging prompt (classic new-post screen). | |
| 28 | 32 | * |
| 29 | 33 | * When we know a user is explicitly answering a prompt, pre-populate the post meta to mark the post as a prompt response, |
| 30 | 34 | * in case they decide to remove the block from the post content, preventing they meta from being added later. |
| 31 | 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 | + * | |
| 32 | 41 | * Called on `wp_insert_post` hook. |
| 33 | 42 | * |
| 34 | 43 | * @param int $post_id ID of post being inserted. |
| 35 | 44 | * @return void |
| @@ -34,26 +43,75 @@ | ||
| 34 | 43 | * @param int $post_id ID of post being inserted. |
| 35 | 44 | * @return void |
| 36 | 45 | */ |
| 37 | 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 | + | |
| 38 | 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. |
| 39 | - $prompt_id = isset( $_GET['answer_prompt'] ) && absint( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : false; | |
| 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 | +} | |
| 40 | 61 | |
| 41 | - if ( ! jetpack_is_new_post_screen() || ! $prompt_id ) { | |
| 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 ) { | |
| 42 | 77 | return; |
| 43 | 78 | } |
| 44 | 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 ) { | |
| 45 | 100 | // Make sure the prompt exists. |
| 46 | 101 | $prompt = jetpack_get_blogging_prompt_by_id( $prompt_id ); |
| 47 | 102 | |
| 48 | - if ( $prompt ) { | |
| 49 | - update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id ); | |
| 50 | - wp_add_post_tags( $post_id, array( 'dailyprompt', "dailyprompt-$prompt_id" ) ); | |
| 103 | + if ( ! $prompt ) { | |
| 104 | + return; | |
| 51 | 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 | + } | |
| 52 | 112 | } |
| 53 | 113 | |
| 54 | -add_action( 'wp_insert_post', 'jetpack_setup_blogging_prompt_response' ); | |
| 55 | - | |
| 56 | 114 | /** |
| 57 | 115 | * When a published posts answers a blogging prompt, store the prompt id in the post meta. |
| 58 | 116 | * |
| 59 | 117 | * @param int $post_id Post ID. |
| @@ -66,16 +124,16 @@ | ||
| 66 | 124 | if ( ! $post instanceof WP_Post ) { |
| 67 | 125 | return; |
| 68 | 126 | } |
| 69 | 127 | |
| 70 | - $post_type = isset( $post->post_type ) ? $post->post_type : null; | |
| 71 | - $post_content = isset( $post->post_content ) ? $post->post_content : null; | |
| 128 | + $post_type = $post->post_type ?? null; | |
| 129 | + $post_content = $post->post_content ?? null; | |
| 72 | 130 | |
| 73 | 131 | if ( 'post' !== $post_type || ! $post_content ) { |
| 74 | 132 | return; |
| 75 | 133 | } |
| 76 | 134 | |
| 77 | - $new_status = isset( $post->post_status ) ? $post->post_status : null; | |
| 135 | + $new_status = $post->post_status ?? null; | |
| 78 | 136 | $old_status = $post_before && isset( $post_before->post_status ) ? $post_before->post_status : null; |
| 79 | 137 | |
| 80 | 138 | // Make sure we are publishing a post, and it's not already published. |
| 81 | 139 | if ( 'publish' !== $new_status || 'publish' === $old_status ) { |
| @@ -81,21 +139,47 @@ | ||
| 81 | 139 | if ( 'publish' !== $new_status || 'publish' === $old_status ) { |
| 82 | 140 | return; |
| 83 | 141 | } |
| 84 | 142 | |
| 85 | - $blocks = parse_blocks( $post->post_content ); | |
| 86 | - foreach ( $blocks as $block ) { | |
| 87 | - if ( 'jetpack/blogging-prompt' === $block['blockName'] ) { | |
| 88 | - $prompt_id = isset( $block['attrs']['promptId'] ) ? absint( $block['attrs']['promptId'] ) : null; | |
| 89 | - $has_prompt_tag = has_tag( 'dailyprompt', $post ) || ( $prompt_id && has_tag( "dailyprompt-{$prompt_id}", $post ) ); | |
| 143 | + $scanner = \Automattic\Block_Scanner::create( $post->post_content ); | |
| 144 | + if ( ! $scanner ) { | |
| 145 | + return; | |
| 146 | + } | |
| 90 | 147 | |
| 91 | - if ( $prompt_id && $has_prompt_tag && count( $blocks ) > 1 ) { | |
| 92 | - update_post_meta( $post->ID, '_jetpack_blogging_prompt_key', $prompt_id ); | |
| 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; | |
| 93 | 162 | } |
| 94 | 163 | |
| 95 | - break; | |
| 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 | + } | |
| 96 | 168 | } |
| 97 | 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 ); | |
| 98 | 182 | } |
| 99 | 183 | |
| 100 | 184 | add_action( 'wp_after_insert_post', 'jetpack_mark_if_post_answers_blogging_prompt', 10, 4 ); |
| 101 | 185 | |
| @@ -103,12 +187,41 @@ | ||
| 103 | 187 | * Utility functions. |
| 104 | 188 | */ |
| 105 | 189 | |
| 106 | 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 | +/** | |
| 107 | 220 | * Retrieve a blogging prompt by prompt ID. |
| 108 | 221 | * |
| 109 | 222 | * @param int $prompt_id ID of the prompt fetch. |
| 110 | - * @return stdClass|null Prompt object or null. | |
| 223 | + * @return array|null Prompt object or null. | |
| 111 | 224 | */ |
| 112 | 225 | function jetpack_get_blogging_prompt_by_id( $prompt_id ) { |
| 113 | 226 | // Ensure the REST API endpoint we need is loaded. |
| 114 | 227 | require_once __DIR__ . '/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php'; |
| @@ -113,12 +226,14 @@ | ||
| 113 | 226 | // Ensure the REST API endpoint we need is loaded. |
| 114 | 227 | require_once __DIR__ . '/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php'; |
| 115 | 228 | |
| 116 | 229 | $locale = get_locale(); |
| 117 | - $route = sprintf( '/wpcom/v3/blogging-prompts/%d', $prompt_id ); | |
| 230 | + $route = jetpack_get_blogging_prompt_route( $prompt_id ); | |
| 118 | 231 | |
| 119 | 232 | $request = new WP_REST_Request( 'GET', $route ); |
| 120 | 233 | $request->set_param( '_locale', $locale ); |
| 234 | + $request->set_param( 'force_year', gmdate( 'Y' ) ); | |
| 235 | + | |
| 121 | 236 | $response = rest_do_request( $request ); |
| 122 | 237 | |
| 123 | 238 | if ( $response->is_error() || WP_Http::OK !== $response->get_status() ) { |
| 124 | 239 | return null; |