-
Notifications
You must be signed in to change notification settings - Fork 846
Editor: add blogging prompts to posts #26680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6cf44da
0089758
5794692
2288c9e
b3107e0
1f2f280
de77c08
1d34a45
76a92fb
8bc9466
09783c1
a9a7d04
a222c03
77ba7f0
70455ab
7b7b33f
5353ca8
2ca729f
4d317a0
9d70257
d98ae99
a6d9eaa
577331e
390e3c5
fc9ccc7
5b9e8c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,16 @@ | ||
| <?php | ||
| /** | ||
| * Used by the blogging prompt feature of the mobile app. | ||
| * Used by the blogging prompt feature. | ||
| * | ||
| * @package automattic/jetpack | ||
| */ | ||
|
|
||
| add_filter( 'rest_api_allowed_public_metadata', 'jetpack_blogging_prompts_add_meta_data' ); | ||
| /** | ||
| * Hooked functions. | ||
| */ | ||
|
|
||
| /** | ||
| * Adds the blogging prompt key post metq to the list of allowed post meta to be updated by rest api. | ||
| * Adds the blogging prompt key post meta to the list of allowed post meta to be updated by rest api. | ||
| * | ||
| * @param array $keys Array of post meta keys that are allowed public metadata. | ||
| * | ||
|
|
@@ -18,3 +20,209 @@ function jetpack_blogging_prompts_add_meta_data( $keys ) { | |
| $keys[] = '_jetpack_blogging_prompt_key'; | ||
| return $keys; | ||
| } | ||
|
|
||
| add_filter( 'rest_api_allowed_public_metadata', 'jetpack_blogging_prompts_add_meta_data' ); | ||
|
|
||
| /** | ||
| * Sets up a new post as an answer to a blogging prompt. | ||
| * | ||
| * Called on `wp_insert_post` hook. | ||
| * | ||
| * @param int $post_id ID of post being inserted. | ||
| * @return void | ||
| */ | ||
| function jetpack_setup_blogging_prompt_response( $post_id ) { | ||
| // phpcs:ignore WordPress.Security.NonceVerification.Recommended | ||
| $prompt_id = isset( $_GET['answer_prompt'] ) && absint( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : false; | ||
roo2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if ( ! jetpack_is_new_post_screen() || ! $prompt_id ) { | ||
roo2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
|
|
||
| if ( jetpack_is_valid_blogging_prompt( $prompt_id ) ) { | ||
roo2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id ); | ||
| wp_add_post_tags( $post_id, 'dailyprompt' ); | ||
roo2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| add_action( 'wp_insert_post', 'jetpack_setup_blogging_prompt_response' ); | ||
|
|
||
| /** | ||
| * Utility functions. | ||
| */ | ||
|
|
||
| /** | ||
| * Retrieve daily blogging prompts from the wpcom API and cache them. | ||
| * | ||
| * @param int $time Unix timestamp representing the day for which to get blogging prompts. | ||
| * @return stdClass[] Array of blogging prompt objects. | ||
| */ | ||
| function jetpack_get_daily_blogging_prompts( $time = 0 ) { | ||
| // Default to the current time in the site's timezone. | ||
| $timestamp = $time ? $time : current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested | ||
|
|
||
| // Include prompts from the previous day, just in case someone has an outdated prompt id. | ||
| $day_before = date_i18n( 'Y-m-d', $timestamp - DAY_IN_SECONDS ); | ||
| $locale = jetpack_get_mag16_locale(); | ||
| $transient_key = 'jetpack_blogging_prompt_' . $day_before . '_' . $locale; | ||
| $daily_prompts = get_transient( $transient_key ); | ||
|
|
||
| // Return the cached prompt, if we have it. Otherwise fetch it from the API. | ||
| if ( false !== $daily_prompts ) { | ||
| return $daily_prompts; | ||
| } | ||
|
|
||
| $blog_id = \Jetpack_Options::get_option( 'id' ); | ||
| $path = '/sites/' . $blog_id . '/blogging-prompts?from=' . $day_before . '&number=10&_locale=' . $locale; | ||
| $args = array( | ||
| 'headers' => array( | ||
| 'Content-Type' => 'application/json', | ||
| 'X-Forwarded-For' => ( new \Automattic\Jetpack\Status\Visitor() )->get_ip( true ), | ||
| ), | ||
| // `method` and `url` are needed for using `WPCOM_API_Direct::do_request` | ||
| // `wpcom_json_api_request_as_user` will generate and overwrite these. | ||
| 'method' => \WP_REST_Server::READABLE, | ||
| 'url' => JETPACK__WPCOM_JSON_API_BASE . '/wpcom/v2' . $path, | ||
| ); | ||
|
|
||
| if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | ||
| // This will load the library, but it may be too late to automatically load any endpoints using WPCOM_API_Direct::register_endpoints. | ||
| // In that case, call `wpcom_rest_api_v2_load_plugin_files( 'wp-content/rest-api-plugins/endpoints/blogging-prompts.php' )` | ||
| // on the `init` hook to load the blogging-prompts endpoint before calling this function. | ||
| require_lib( 'wpcom-api-direct' ); | ||
| $response = \WPCOM_API_Direct::do_request( $args ); | ||
| } else { | ||
| $response = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user( $path, 'v2', $args, null, 'wpcom' ); | ||
| } | ||
| $response_status = wp_remote_retrieve_response_code( $response ); | ||
|
|
||
| if ( is_wp_error( $response ) || $response_status !== \WP_Http::OK ) { | ||
| return null; | ||
| } | ||
|
|
||
| $body = json_decode( wp_remote_retrieve_body( $response ) ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be error checking here in case the |
||
| $prompts = $body->prompts; | ||
| set_transient( $transient_key, $prompts, DAY_IN_SECONDS ); | ||
|
|
||
| return $prompts; | ||
| } | ||
|
|
||
| /** | ||
| * Trim language code for to match one of the 16 languages translated for WP.com | ||
| * | ||
| * The blogging-prompts API currently only has translations for these languages, and | ||
| * won't fall back to generic versions. e.g. fr_BE will return English, so we trim to | ||
| * fr to get the French translations. | ||
| * | ||
| * @return string | ||
| */ | ||
| function jetpack_get_mag16_locale() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function seems a bit unfortunate to have in Jetpack rather than having the server side do the fix-up. If you were to add fr_BE, for example, you'd have to wait for the monthly Jetpack release (and for people to update to it) for standalone sites to get it. While if this were done on the server side instead, they'd start getting it as soon as you updated it there.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point. The blogging-prompts endpoint is relying on a global |
||
| $locale = get_locale(); | ||
|
|
||
| if ( ! in_array( strtolower( $locale ), array( 'zh_cn', 'zh_tw', 'pt_br' ), true ) ) { | ||
| // Trim the locale from the end of the language code, unless we specifically translate that version of the language. | ||
| return preg_replace( '/(_.*)$/i', '', $locale ); | ||
| } elseif ( 'pt' === $locale ) { | ||
| // We have Portuguese (Brazil), but not Portuguese (Portugal) translations. | ||
| return 'pt_br'; | ||
| } | ||
|
|
||
| return $locale; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the site has publish posts or plans to publish posts. | ||
| * | ||
| * @return bool | ||
| */ | ||
| function jetpack_has_or_will_publish_posts() { | ||
| // Lets count the posts. | ||
| $count_posts_object = wp_count_posts( 'post' ); | ||
| $count_posts = (int) $count_posts_object->publish + (int) $count_posts_object->future + (int) $count_posts_object->draft; | ||
|
|
||
| return $count_posts_object->publish >= 2 || $count_posts >= 100; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the site has a posts page or shows posts on the front page. | ||
| * | ||
| * @return bool | ||
| */ | ||
| function jetpack_has_posts_page() { | ||
| // The site is set up to be a blog. | ||
| if ( 'posts' === get_option( 'show_on_front' ) ) { | ||
| return true; | ||
| } | ||
|
|
||
| // There is a page set to show posts. | ||
| $is_posts_page_set = (int) get_option( 'page_for_posts' ) > 0; | ||
| if ( $is_posts_page_set ) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if site had the "Write" intent set when created. | ||
| * | ||
| * @return bool | ||
| */ | ||
| function jetpack_has_write_intent() { | ||
| return 'write' === get_option( 'site_intent', '' ); | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the current screen (in wp-admin) is creating a new post. | ||
| * | ||
| * /wp-admin/post-new.php | ||
| * | ||
| * @return bool | ||
| */ | ||
| function jetpack_is_new_post_screen() { | ||
| global $current_screen; | ||
|
|
||
| if ( | ||
| $current_screen instanceof \WP_Screen && | ||
| 'add' === $current_screen->action && | ||
| 'post' === $current_screen->post_type | ||
| ) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the site might have a blog. | ||
| * | ||
| * @return bool | ||
| */ | ||
| function jetpack_is_potential_blogging_site() { | ||
| return jetpack_has_write_intent() || jetpack_has_posts_page() || jetpack_has_or_will_publish_posts(); | ||
|
||
| } | ||
|
|
||
| /** | ||
| * Checks if the given prompt id is included in today's blogging prompts. | ||
| * | ||
| * Would be best to use the API to check if the prompt id is valid for any day, | ||
| * but for now we're only using one prompt per day. | ||
| * | ||
| * @param int $prompt_id id of blogging prompt. | ||
| * @return bool | ||
| */ | ||
| function jetpack_is_valid_blogging_prompt( $prompt_id ) { | ||
| $daily_prompts = jetpack_get_daily_blogging_prompts(); | ||
|
|
||
| if ( ! $daily_prompts ) { | ||
| return false; | ||
| } | ||
|
|
||
| foreach ( $daily_prompts as $prompt ) { | ||
| if ( $prompt->id === $prompt_id ) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: enhancement | ||
|
|
||
| Adds an experimental editor extension that displays a placeholder blogging prompt when starting a new post |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| Significance: patch | ||
| Type: other | ||
| Comment: Changelog entry already provided from previous PR | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| <?php | ||
| /** | ||
| * Blogging prompts. | ||
| * | ||
| * @since $$next-version$$ | ||
| * | ||
| * @package automattic/jetpack | ||
| */ | ||
|
|
||
| namespace Automattic\Jetpack\Extensions\BloggingPrompts; | ||
|
|
||
| use Automattic\Jetpack\Blocks; | ||
|
|
||
| const FEATURE_NAME = 'blogging-prompts'; | ||
| const BLOCK_NAME = 'jetpack/' . FEATURE_NAME; | ||
|
|
||
| /** | ||
| * Registers the blogging prompt integration for the block editor. | ||
| */ | ||
| function register_extension() { | ||
| Blocks::jetpack_register_block( BLOCK_NAME ); | ||
|
|
||
| // Load the blogging-prompts endpoint here on init so its route will be registered. | ||
| // We can use it with `WPCOM_API_Direct::do_request` to avoid a network request on Simple Sites. | ||
|
||
| if ( defined( 'IS_WPCOM' ) && IS_WPCOM && should_load_blogging_prompts() ) { | ||
| wpcom_rest_api_v2_load_plugin_files( 'wp-content/rest-api-plugins/endpoints/blogging-prompts.php' ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Loads the blogging prompt extension within the editor, if appropriate. | ||
| */ | ||
| function inject_blogging_prompts() { | ||
| // Return early if we are not in the block editor. | ||
| if ( ! wp_should_load_block_editor_scripts_and_styles() ) { | ||
| return; | ||
| } | ||
|
|
||
| // Or if we aren't creating a new post. | ||
| if ( ! jetpack_is_new_post_screen() ) { | ||
| return; | ||
| } | ||
|
|
||
| // And only for blogging sites or those explicitly responding to the prompt. | ||
| if ( should_load_blogging_prompts() ) { | ||
| $daily_prompts = wp_json_encode( jetpack_get_daily_blogging_prompts() ); | ||
|
|
||
| // See p7H4VZ-2cf-p2 for why prompt data is escaped this way. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the post itself notes,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for clarifying what's currently considered safe. I'll refactor. |
||
| if ( $daily_prompts ) { | ||
| wp_add_inline_script( 'jetpack-blocks-editor', 'var Jetpack_BloggingPrompts = JSON.parse( decodeURIComponent( "' . rawurlencode( $daily_prompts ) . '" ) );', 'before' ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines if the blogging prompts extension should be loaded in the editor. | ||
| * | ||
| * @return bool | ||
| */ | ||
| function should_load_blogging_prompts() { | ||
| return jetpack_has_write_intent() || | ||
roo2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| jetpack_has_posts_page() || | ||
| // phpcs:ignore WordPress.Security.NonceVerification.Recommended | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above, why can we ignore nonce verification here? |
||
| ( isset( $_GET['answer_prompt'] ) && absint( $_GET['answer_prompt'] ) ); | ||
| } | ||
|
|
||
| add_action( 'init', __NAMESPACE__ . '\register_extension' ); | ||
| add_action( 'enqueue_block_assets', __NAMESPACE__ . '\inject_blogging_prompts' ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { createBlock } from '@wordpress/blocks'; | ||
| import { dispatch } from '@wordpress/data'; | ||
| import { waitForEditor } from '../../shared/wait-for-editor'; | ||
|
|
||
| async function insertTemplate( prompt, embedPrompt = false ) { | ||
| await waitForEditor(); | ||
|
|
||
| const { insertBlocks } = dispatch( 'core/block-editor' ); | ||
| const bloggingPromptBlocks = embedPrompt | ||
| ? [ createBlock( 'core/pullquote', { value: prompt.text } ), createBlock( 'core/paragraph' ) ] | ||
| : createBlock( 'core/paragraph', { placeholder: prompt.text }, [] ); | ||
|
|
||
| insertBlocks( bloggingPromptBlocks, 0, undefined, false ); | ||
| } | ||
|
|
||
| function initBloggingPrompts() { | ||
| const prompts = window.Jetpack_BloggingPrompts; | ||
| if ( ! Array.isArray( prompts ) || ! prompts[ 0 ] ) { | ||
| return; | ||
| } | ||
|
|
||
| const urlQuery = new URLSearchParams( document.location.search ); | ||
| const answerPrompt = urlQuery.get( 'answer_prompt' ) ?? '0'; | ||
| const answerPromptId = parseInt( answerPrompt ); | ||
|
|
||
| // Try to find the prompt by id, otherwise just default to the first prompt for today. | ||
| // The current list of prompts starts from yesteday, so today's is the second prompt. | ||
| const prompt = prompts.find( p => p.id === answerPromptId ) ?? prompts[ 1 ]; | ||
|
|
||
| if ( prompt ) { | ||
| insertTemplate( prompt, !! answerPromptId ); | ||
| } | ||
| } | ||
|
|
||
| initBloggingPrompts(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| class WP_Test_Jetpack_Blogging_Prompts extends WP_UnitTestCase { | ||
| public function test_jetpack_get_mag16_locale() { | ||
| $valid_locales = array( 'ar', 'zh_cn', 'tr', 'zh_tw', 'nl', 'en', 'fr', 'de', 'he', 'id', 'it', 'ja', 'ko', 'pt_br', 'ru', 'es', 'sv' ); | ||
| $locales_to_test = array( 'zh_cn', 'pt', 'fr', 'fr_be', 'en_us', 'nl_be' ); | ||
|
|
||
| foreach ( $locales_to_test as $locale ) { | ||
| switch_to_locale( $locale ); | ||
| $site_locale = jetpack_get_mag16_locale(); | ||
| $this->assertContains( strtolower( $site_locale ), $valid_locales ); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is nonce verification being ignored here? We try to include a comment explaining it.
See p9dueE-4z8-p2 for more info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are generating the link to be clicked from a WP.com notification, so it's possible the nonce would change before the user clicks the link. I'll add a comment.
Not ideal, as we are "setting a field," in the form of a tag, post_meta with the prompt id, and a prefilled pull quote block, though no data is saved until the user publishes. Please let me know if you have other thoughts or a better solution!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as it's well-explained in the comment, I think that's a good start. 👍
Do I have this right? There's a link in the notification, and clicking the link winds up calling this function. You can't put a nonce in the link, because it might be a while in between when the link is generated and when it's clicked. The "attack" that a nonce would prevent would be some third party tricking the blog admin's browser into following the link, maybe repeatedly to flood the site with these posts. Ideally the comment should address why that potential attack isn't something to actually worry about.