Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6cf44da
[not verified] Add writing prompts to posts
creativecoder Oct 6, 2022
0089758
[not verified] changelog
creativecoder Oct 6, 2022
5794692
[not verified] Removes logging
creativecoder Oct 6, 2022
2288c9e
[not verified] Removes uneeded index.js file
creativecoder Oct 6, 2022
b3107e0
[not verified] Use wp_rand for PHP < 7 compatibility
creativecoder Oct 6, 2022
1f2f280
[not verified] Addresses feedback
creativecoder Oct 19, 2022
de77c08
[not verified] Retrieves writing prompts from the REST API
creativecoder Oct 19, 2022
1d34a45
[not verified] Limits writing prompts to posts only
creativecoder Oct 19, 2022
76a92fb
[not verified] Addresses feedback
creativecoder Oct 21, 2022
8bc9466
[not verified] Rename feature to "Blogging" prompts for consistency w…
creativecoder Oct 27, 2022
09783c1
[not verified] Only show writing prompt if site is potentially a blog
creativecoder Oct 27, 2022
a9a7d04
[not verified] Adds compatibility for Simple Sites
creativecoder Oct 31, 2022
a222c03
[not verified] Use "blogging" prompts everywhere, for consistency wit…
creativecoder Oct 31, 2022
77ba7f0
[not verified] Adds meta and tag to posts that answer a blogging prompt
creativecoder Nov 3, 2022
70455ab
[not verified] Allows specifying which checks are used to determine a…
creativecoder Nov 3, 2022
7b7b33f
[not verified] Breaks out functions for determining a blogging site
creativecoder Nov 7, 2022
5353ca8
[not verified] Use _locale parameter for fetching prompts in site lan…
creativecoder Nov 7, 2022
2ca729f
[not verified] Show prompt by id
creativecoder Nov 7, 2022
4d317a0
Move blogging prompts extension to beta
creativecoder Nov 7, 2022
9d70257
[not verified] Corrects page_for_posts comment
creativecoder Nov 15, 2022
d98ae99
[not verified] Clarifies comment about loading direct request library
creativecoder Nov 15, 2022
a6d9eaa
Allows answering yesterday's prompt
creativecoder Nov 15, 2022
577331e
Defaults editor placeholder to today's prompt
creativecoder Nov 15, 2022
390e3c5
Ensure mag16 language for blogging prompts
creativecoder Nov 17, 2022
fc9ccc7
Allow specifying the timestamp to get blogging prompts for that day
creativecoder Nov 17, 2022
5b9e8c5
Blogging prompts: move to experimental blocks (#27483)
creativecoder Nov 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 211 additions & 3 deletions projects/plugins/jetpack/_inc/blogging-prompts.php
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.
*
Expand All @@ -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
Copy link
Contributor

@anomiex anomiex Nov 21, 2022

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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.

$prompt_id = isset( $_GET['answer_prompt'] ) && absint( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : false;

if ( ! jetpack_is_new_post_screen() || ! $prompt_id ) {
return;
}

if ( jetpack_is_valid_blogging_prompt( $prompt_id ) ) {
update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id );
wp_add_post_tags( $post_id, 'dailyprompt' );
}
}

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 ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be error checking here in case the json_decode() returns null?

$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() {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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, but I'll see about customizing it just for this endpoint and pulling out this function. FYI, some related conversation: D91251-code#1932120

$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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for splitting it out like this even though we changed direction so it's not strictly necessary, I still think it makes it more readable. Related discussion was in this thread p1667869365431739/1667217622.476049-slack-C03NLNTPZ2T

}

/**
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work BTW figuring out how to avoid the request on wpcom 👌

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the post itself notes, json_encode() is safe to use here with the right flags, and the problem back in February 2019 was that WordPress still supported PHP 5.2 which didn't have those flags. We've been at a minimum of 5.6 now for a long time, so just use wp_json_encode( ..., JSON_HEX_TAG ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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() ||
jetpack_has_posts_page() ||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
8 changes: 7 additions & 1 deletion projects/plugins/jetpack/extensions/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@
"videopress/video",
"videopress/video-chapters"
],
"experimental": [ "anchor-fm", "premium-content", "conversation", "dialogue" ],
"experimental": [
"anchor-fm",
"blogging-prompts",
"premium-content",
"conversation",
"dialogue"
],
"no-post-editor": [
"business-hours",
"button",
Expand Down
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 );
}
}
}