Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.81% covered (danger)
43.81%
46 / 105
20.00% covered (danger)
20.00%
2 / 10
CRAP
n/a
0 / 0
jetpack_blogging_prompts_add_meta_data
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
jetpack_setup_blogging_prompt_response
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
7.07
jetpack_mark_if_post_answers_blogging_prompt
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
25
jetpack_get_blogging_prompt_by_id
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_get_daily_blogging_prompts
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
jetpack_has_or_will_publish_posts
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_has_posts_page
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_has_write_intent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_is_new_post_screen
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
jetpack_is_potential_blogging_site
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Used by the blogging prompt feature.
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! 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 */
23function jetpack_blogging_prompts_add_meta_data( $keys ) {
24    $keys[] = '_jetpack_blogging_prompt_key';
25    return $keys;
26}
27
28add_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.
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 * Called on `wp_insert_post` hook.
37 *
38 * @param int $post_id ID of post being inserted.
39 * @return void
40 */
41function jetpack_setup_blogging_prompt_response( $post_id ) {
42    // 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.
43    $prompt_id = isset( $_GET['answer_prompt'] ) && absint( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : false;
44
45    if ( ! jetpack_is_new_post_screen() || ! $prompt_id ) {
46        return;
47    }
48
49    // Make sure the prompt exists.
50    $prompt = jetpack_get_blogging_prompt_by_id( $prompt_id );
51
52    if ( $prompt ) {
53        update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id );
54        wp_add_post_tags( $post_id, array( 'dailyprompt', "dailyprompt-$prompt_id" ) );
55        if ( array_key_exists( 'bloganuary_id', $prompt ) ) {
56            wp_add_post_tags( $post_id, array( 'bloganuary', $prompt['bloganuary_id'] ) );
57        }
58    }
59}
60
61add_action( 'wp_insert_post', 'jetpack_setup_blogging_prompt_response' );
62
63/**
64 * When a published posts answers a blogging prompt, store the prompt id in the post meta.
65 *
66 * @param int          $post_id     Post ID.
67 * @param WP_Post      $post        Post object.
68 * @param bool         $update      Whether this is an existing post being updated.
69 * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
70 *                                  to the update for updated posts.
71 */
72function jetpack_mark_if_post_answers_blogging_prompt( $post_id, $post, $update, $post_before ) {
73    if ( ! $post instanceof WP_Post ) {
74        return;
75    }
76
77    $post_type    = isset( $post->post_type ) ? $post->post_type : null;
78    $post_content = isset( $post->post_content ) ? $post->post_content : null;
79
80    if ( 'post' !== $post_type || ! $post_content ) {
81        return;
82    }
83
84    $new_status = isset( $post->post_status ) ? $post->post_status : null;
85    $old_status = $post_before && isset( $post_before->post_status ) ? $post_before->post_status : null;
86
87    // Make sure we are publishing a post, and it's not already published.
88    if ( 'publish' !== $new_status || 'publish' === $old_status ) {
89        return;
90    }
91
92    $scanner = \Automattic\Block_Scanner::create( $post->post_content );
93    if ( ! $scanner ) {
94        return;
95    }
96
97    $prompt_id          = null;
98    $total_blocks       = 0;
99    $found_prompt_block = false;
100
101    while ( $scanner->next_delimiter() ) {
102        if ( $scanner->opens_block() ) {
103            ++$total_blocks;
104
105            if ( ! $found_prompt_block && $scanner->is_block_type( 'jetpack/blogging-prompt' ) ) {
106                $attributes = $scanner->allocate_and_return_parsed_attributes();
107                if ( $attributes && isset( $attributes['promptId'] ) ) {
108                    $prompt_id = absint( $attributes['promptId'] );
109                }
110                $found_prompt_block = true;
111            }
112
113            // Early exit: if we found the prompt and have >1 blocks, we have all info needed
114            if ( $found_prompt_block && $total_blocks > 1 ) {
115                break;
116            }
117        }
118    }
119
120    if ( ! $found_prompt_block || ! $prompt_id || $total_blocks <= 1 ) {
121        return;
122    }
123
124    $has_prompt_tag = has_tag( 'dailyprompt', $post ) || has_tag( "dailyprompt-{$prompt_id}", $post );
125
126    if ( ! $has_prompt_tag ) {
127        return;
128    }
129
130    update_post_meta( $post->ID, '_jetpack_blogging_prompt_key', $prompt_id );
131}
132
133add_action( 'wp_after_insert_post', 'jetpack_mark_if_post_answers_blogging_prompt', 10, 4 );
134
135/**
136 * Utility functions.
137 */
138
139/**
140 * Retrieve a blogging prompt by prompt ID.
141 *
142 * @param int $prompt_id ID of the prompt fetch.
143 * @return stdClass|null Prompt object or null.
144 */
145function jetpack_get_blogging_prompt_by_id( $prompt_id ) {
146    // Ensure the REST API endpoint we need is loaded.
147    require_once __DIR__ . '/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php';
148
149    $locale = get_locale();
150    $route  = sprintf( '/wpcom/v3/blogging-prompts/%d', $prompt_id );
151
152    $request = new WP_REST_Request( 'GET', $route );
153    $request->set_param( '_locale', $locale );
154    $request->set_param( 'force_year', gmdate( 'Y' ) );
155
156    $response = rest_do_request( $request );
157
158    if ( $response->is_error() || WP_Http::OK !== $response->get_status() ) {
159        return null;
160    }
161
162    $prompt = $response->get_data();
163
164    return $prompt;
165}
166
167/**
168 * Retrieve daily blogging prompts from the wpcom API and cache them.
169 *
170 * @param int $time Unix timestamp representing the day for which to get blogging prompts.
171 * @return stdClass[]|null Array of blogging prompt objects or null.
172 */
173function jetpack_get_daily_blogging_prompts( $time = 0 ) {
174    $timestamp = $time ? $time : time();
175
176    // Include prompts from the previous day, just in case someone has an outdated prompt id.
177    $day_before    = wp_date( 'Y-m-d', $timestamp - DAY_IN_SECONDS );
178    $locale        = get_locale();
179    $transient_key = 'jetpack_blogging_prompt_' . $day_before . '_' . $locale;
180    $daily_prompts = get_transient( $transient_key );
181
182    // Return the cached prompt, if we have it. Otherwise fetch it from the API.
183    if ( false !== $daily_prompts ) {
184        return $daily_prompts;
185    }
186
187    $blog_id = \Jetpack_Options::get_option( 'id' );
188    $path    = '/sites/' . rawurldecode( $blog_id ) . '/blogging-prompts?from=' . rawurldecode( $day_before ) . '&number=10&_locale=' . rawurldecode( $locale );
189
190    $args = array(
191        'headers' => array(
192            'Content-Type'    => 'application/json',
193            'X-Forwarded-For' => ( new \Automattic\Jetpack\Status\Visitor() )->get_ip( true ),
194        ),
195        // `method` and `url` are needed for using `WPCOM_API_Direct::do_request`
196        // `wpcom_json_api_request_as_user` will generate and overwrite these.
197        'method'  => \WP_REST_Server::READABLE,
198        'url'     => JETPACK__WPCOM_JSON_API_BASE . '/wpcom/v2' . $path,
199    );
200
201    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
202        // This will load the library, but it may be too late to automatically load any endpoints using WPCOM_API_Direct::register_endpoints.
203        // In that case, call `wpcom_rest_api_v2_load_plugin_files( 'wp-content/rest-api-plugins/endpoints/blogging-prompts.php' )`
204        // on the `init` hook to load the blogging-prompts endpoint before calling this function.
205        require_once WP_CONTENT_DIR . '/lib/wpcom-api-direct/wpcom-api-direct.php';
206        $response = \WPCOM_API_Direct::do_request( $args );
207    } else {
208        $response = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user( $path, 'v2', $args, null, 'wpcom' );
209    }
210    $response_status = wp_remote_retrieve_response_code( $response );
211
212    if ( is_wp_error( $response ) || $response_status !== \WP_Http::OK ) {
213        return null;
214    }
215
216    $body = json_decode( wp_remote_retrieve_body( $response ) );
217
218    if ( ! $body || ! isset( $body->prompts ) ) {
219        return null;
220    }
221
222    $prompts = $body->prompts;
223    set_transient( $transient_key, $prompts, DAY_IN_SECONDS );
224
225    return $prompts;
226}
227
228/**
229 * Determines if the site has publish posts or plans to publish posts.
230 *
231 * @return bool
232 */
233function jetpack_has_or_will_publish_posts() {
234    // Lets count the posts.
235    $count_posts_object = wp_count_posts( 'post' );
236    $count_posts        = (int) $count_posts_object->publish + (int) $count_posts_object->future + (int) $count_posts_object->draft;
237
238    return $count_posts_object->publish >= 2 || $count_posts >= 100;
239}
240
241/**
242 * Determines if the site has a posts page or shows posts on the front page.
243 *
244 * @return bool
245 */
246function jetpack_has_posts_page() {
247    // The site is set up to be a blog.
248    if ( 'posts' === get_option( 'show_on_front' ) ) {
249        return true;
250    }
251
252    // There is a page set to show posts.
253    $is_posts_page_set = (int) get_option( 'page_for_posts' ) > 0;
254    if ( $is_posts_page_set ) {
255        return true;
256    }
257
258    return false;
259}
260
261/**
262 * Determines if site had the "Write" intent set when created.
263 *
264 * @return bool
265 */
266function jetpack_has_write_intent() {
267    return 'write' === get_option( 'site_intent', '' );
268}
269
270/**
271 * Determines if the current screen (in wp-admin) is creating a new post.
272 *
273 * /wp-admin/post-new.php
274 *
275 * @return bool
276 */
277function jetpack_is_new_post_screen() {
278    global $current_screen;
279
280    if (
281        $current_screen instanceof \WP_Screen &&
282        'add' === $current_screen->action &&
283        'post' === $current_screen->post_type
284    ) {
285        return true;
286    }
287
288    return false;
289}
290
291/**
292 * Determines if the site might have a blog.
293 *
294 * @return bool
295 */
296function jetpack_is_potential_blogging_site() {
297    return jetpack_has_write_intent() || jetpack_has_posts_page() || jetpack_has_or_will_publish_posts();
298}