Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 61 additions & 1 deletion includes/api/class-plugins-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ public function register_routes() {
]
);

// Register newspack/v1/handoff endpoint for URL-based handoff.
register_rest_route(
$this->namespace,
'/handoff',
[
[
'methods' => 'POST',
'callback' => [ $this, 'handoff_to_url' ],
'permission_callback' => [ $this, 'handoff_item_permissions_check' ],
],
]
);

// Register newspack/v1/plugins/some-plugin/handoff endpoint.
register_rest_route(
$this->namespace,
Expand Down Expand Up @@ -314,6 +327,51 @@ public function uninstall_item( $request ) {
return $this->get_item( $request );
}

/**
* Handoff to an arbitrary admin URL.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function handoff_to_url( $request ) {
$destination_url = $request->get_param( 'destinationUrl' );
if ( empty( $destination_url ) ) {
return new \WP_Error( 'newspack_handoff_missing_url', __( 'destinationUrl is required.', 'newspack-plugin' ), [ 'status' => 400 ] );
}

// Reject external URLs to prevent open-redirect attacks.
$parsed_destination = wp_parse_url( $destination_url );
if ( ! empty( $parsed_destination['host'] ) ) {
$site_host = wp_parse_url( admin_url(), PHP_URL_HOST );
if ( $parsed_destination['host'] !== $site_host ) {
return new \WP_Error( 'newspack_handoff_invalid_url', __( 'destinationUrl must be a same-site URL.', 'newspack-plugin' ), [ 'status' => 400 ] );
}
}

$handoff_return_url = $request->get_param( 'handoffReturnUrl' );
$show_on_block_editor = $request->get_param( 'showOnBlockEditor' );
$banner_text = (string) $request->get_param( 'bannerText' );
$banner_button_text = (string) $request->get_param( 'bannerButtonText' );

update_option( NEWSPACK_HANDOFF, 'url' );
update_option( NEWSPACK_HANDOFF_SHOW_ON_BLOCK_EDITOR, (bool) $show_on_block_editor );
update_option( NEWSPACK_HANDOFF_BANNER_TEXT, sanitize_text_field( $banner_text ) );
update_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, sanitize_text_field( $banner_button_text ) );
if ( ! empty( $handoff_return_url ) ) {
update_option( NEWSPACK_HANDOFF_RETURN_URL, esc_url( $handoff_return_url ) );
}

$parsed_url = wp_parse_url( $destination_url );
if ( ! empty( $parsed_url['query'] ) ) {
wp_parse_str( $parsed_url['query'], $query_params );
if ( ! empty( $query_params['page'] ) ) {
update_option( NEWSPACK_HANDOFF_DESTINATION_PAGE, sanitize_text_field( $query_params['page'] ) );
}
}

return rest_ensure_response( [ 'HandoffLink' => esc_url_raw( $destination_url ) ] );
}

/**
* Handoff to a managed plugin.
*
Expand All @@ -329,7 +387,9 @@ public function handoff_item( $request ) {
}

$show_on_block_editor = $request->get_param( 'showOnBlockEditor' );
Handoff_Banner::register_handoff_for_plugin( $slug, (bool) $show_on_block_editor );
$banner_text = (string) $request->get_param( 'bannerText' );
$banner_button_text = (string) $request->get_param( 'bannerButtonText' );
Handoff_Banner::register_handoff_for_plugin( $slug, (bool) $show_on_block_editor, $banner_text, $banner_button_text );
$managed_plugins = Plugin_Manager::get_managed_plugins();

$response = $managed_plugins[ $slug ];
Expand Down
146 changes: 129 additions & 17 deletions includes/class-handoff-banner.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
define( 'NEWSPACK_HANDOFF', 'newspack_handoff' );
define( 'NEWSPACK_HANDOFF_RETURN_URL', 'newspack_handoff_return_url' );
define( 'NEWSPACK_HANDOFF_SHOW_ON_BLOCK_EDITOR', 'newspack_handoff_show_on_block_editor' );
define( 'NEWSPACK_HANDOFF_BANNER_TEXT', 'newspack_handoff_banner_text' );
define( 'NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT', 'newspack_handoff_banner_button_text' );
define( 'NEWSPACK_HANDOFF_DESTINATION_PAGE', 'newspack_handoff_destination_page' );

/**
* Manages the API as a whole.
Expand All @@ -25,11 +28,13 @@ public function __construct() {
add_action( 'current_screen', [ $this, 'clear_handoff_url' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ], 1 );
add_action( 'enqueue_block_editor_assets', [ $this, 'insert_block_editor_handoff_banner' ] );
add_action( 'admin_notices', [ $this, 'insert_handoff_banner' ], -9998 );
add_action( 'in_admin_header', [ $this, 'insert_handoff_banner' ] );
add_action( 'newspack_before_wizard_content', [ $this, 'insert_handoff_banner_static' ] );
}

/**
* Render element into which Handoff Banner will be rendered.
* Render element into which Handoff Banner will be rendered via JS.
* Used on non-wizard admin pages.
*
* @return void.
*/
Expand All @@ -38,7 +43,73 @@ public function insert_handoff_banner() {
return;
}

printf( "<div id='newspack-handoff-banner' data-primary_button_url='%s'></div>", esc_url( get_option( NEWSPACK_HANDOFF_RETURN_URL ) ) );
// On Newspack wizard pages the static banner is rendered via newspack_before_wizard_content.
$screen = get_current_screen();
if ( $screen && stristr( $screen->id, 'newspack' ) ) {
return;
}

printf(
"<div id='newspack-handoff-banner' data-primary_button_url='%s' data-banner_text='%s' data-banner_button_text='%s'></div>",
esc_url( get_option( NEWSPACK_HANDOFF_RETURN_URL ) ),
esc_attr( get_option( NEWSPACK_HANDOFF_BANNER_TEXT, '' ) ),
esc_attr( get_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, '' ) )
);
}

/**
* Render a fully server-side Handoff Banner.
* Used on Newspack wizard pages where the JS-based banner cannot be used.
*
* @return void.
*/
public function insert_handoff_banner_static() {
if ( ! self::needs_handoff_return_ui() ) {
return;
}

$return_url = esc_url( get_option( NEWSPACK_HANDOFF_RETURN_URL ) );
$banner_text = get_option( NEWSPACK_HANDOFF_BANNER_TEXT, '' );
$button_text = get_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, '' );

if ( empty( $banner_text ) ) {
$banner_text = __( 'Return to Newspack after completing configuration', 'newspack-plugin' );
}
if ( empty( $button_text ) ) {
$button_text = __( 'Back to Newspack', 'newspack-plugin' );
}
?>
<div id="newspack-handoff-banner">
<div class="newspack-handoff-banner">
<div class="newspack-handoff-banner__text"><?php echo esc_html( $banner_text ); ?></div>
<div class="newspack-handoff-banner__buttons">
<button
type="button"
class="components-button is-tertiary is-small"
onclick="this.closest('#newspack-handoff-banner').remove()"
>
<?php esc_html_e( 'Dismiss', 'newspack-plugin' ); ?>
</button>
<a href="<?php echo esc_url( $return_url ); ?>" class="components-button is-primary is-small">
<?php echo esc_html( $button_text ); ?>
</a>
</div>
</div>
</div>
<script>
( function() {
var el = document.getElementById( 'newspack-handoff-banner' );
var wpcontent = document.getElementById( 'wpcontent' );
if ( el && wpcontent ) {
var paddingLeft = parseInt( window.getComputedStyle( wpcontent ).paddingLeft, 10 );
if ( paddingLeft ) {
el.style.marginLeft = '-' + paddingLeft + 'px';
el.style.width = 'calc(100% + ' + paddingLeft + 'px)';
}
}
} )();
</script>
<?php
}

/**
Expand All @@ -60,9 +131,11 @@ public function insert_block_editor_handoff_banner() {
true
);

$script_info = [
'text' => __( 'Return to Newspack after completing configuration', 'newspack' ),
'buttonText' => __( 'Back to Newspack', 'newspack' ),
$banner_text = get_option( NEWSPACK_HANDOFF_BANNER_TEXT, '' );
$banner_button_text = get_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, '' );
$script_info = [
'text' => $banner_text ? $banner_text : __( 'Return to Newspack after completing configuration', 'newspack' ),
'buttonText' => $banner_button_text ? $banner_button_text : __( 'Back to Newspack', 'newspack' ),
'returnURL' => esc_url( get_option( NEWSPACK_HANDOFF_RETURN_URL, '' ) ),
];
wp_localize_script( $handle, 'newspack_handoff', $script_info );
Expand All @@ -80,18 +153,19 @@ public function enqueue_styles() {
wp_register_style(
$handle,
Newspack::plugin_url() . '/dist/handoff-banner.css',
[],
[ 'wp-components' ],
NEWSPACK_PLUGIN_VERSION
);
wp_enqueue_style( $handle );

Newspack::load_common_assets();

$asset = include NEWSPACK_ABSPATH . 'dist/handoff-banner.asset.php';
wp_register_script(
$handle,
Newspack::plugin_url() . '/dist/handoff-banner.js',
[ 'wp-element', 'wp-editor', 'wp-components' ],
NEWSPACK_PLUGIN_VERSION,
$asset['dependencies'],
$asset['version'],
true
);
wp_enqueue_script( $handle );
Expand All @@ -102,11 +176,16 @@ public function enqueue_styles() {
*
* @param array $plugin Slug of plugin to be visited.
* @param boolean $show_on_block_editor Whether to show on block editor.
* @param string $banner_text Custom banner body text.
* @param string $banner_button_text Custom banner button text.
* @return void
*/
public static function register_handoff_for_plugin( $plugin, $show_on_block_editor = false ) {
public static function register_handoff_for_plugin( $plugin, $show_on_block_editor = false, $banner_text = '', $banner_button_text = '' ) {
update_option( NEWSPACK_HANDOFF, $plugin );
update_option( NEWSPACK_HANDOFF_SHOW_ON_BLOCK_EDITOR, (bool) $show_on_block_editor );
update_option( NEWSPACK_HANDOFF_BANNER_TEXT, sanitize_text_field( $banner_text ) );
update_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, sanitize_text_field( $banner_button_text ) );
update_option( NEWSPACK_HANDOFF_DESTINATION_PAGE, '' );
}

/**
Expand All @@ -115,10 +194,6 @@ public static function register_handoff_for_plugin( $plugin, $show_on_block_edit
* @return bool
*/
public static function needs_handoff_return_ui() {
if ( get_option( NEWSPACK_SETUP_COMPLETE, true ) ) {
return false;
}

return get_option( NEWSPACK_HANDOFF ) ? true : false;
}

Expand All @@ -132,15 +207,52 @@ public static function needs_block_editor_handoff_return_ui() {
}

/**
* If the current admin page is part of the Newspack dashboard, clear the handoff URL. This ensures the handoff banner won't be shown on Newspack admin pages.
* Clear all handoff-related options.
*
* @return void
*/
private function clear_all_handoff_options() {
update_option( NEWSPACK_HANDOFF, null );
update_option( NEWSPACK_HANDOFF_SHOW_ON_BLOCK_EDITOR, false );
update_option( NEWSPACK_HANDOFF_BANNER_TEXT, '' );
update_option( NEWSPACK_HANDOFF_BANNER_BUTTON_TEXT, '' );
update_option( NEWSPACK_HANDOFF_DESTINATION_PAGE, '' );
}

/**
* Clear the handoff state when navigating away from the destination page.
* Clears on any Newspack screen that isn't the destination, and on any
* non-Newspack screen when a destination page was registered (preventing
* the banner from lingering on unrelated admin pages).
*
* @param WP_Screen $current_screen The current screen object.
* @return void
*/
public function clear_handoff_url( $current_screen ) {
if ( ! self::needs_handoff_return_ui() ) {
return;
}

$destination_page = get_option( NEWSPACK_HANDOFF_DESTINATION_PAGE, '' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';

// Don't clear if we're on the intended destination page.
if ( $destination_page && $current_page === $destination_page ) {
return;
}

// Clear on any Newspack screen that isn't the destination.
if ( stristr( $current_screen->id, 'newspack' ) ) {
update_option( NEWSPACK_HANDOFF, null );
update_option( NEWSPACK_HANDOFF_SHOW_ON_BLOCK_EDITOR, false );
$this->clear_all_handoff_options();
return;
}

// For URL-based handoffs with a known destination page, also clear on any
// non-Newspack page that isn't the destination, to prevent the banner from
// persisting across unrelated admin screens.
if ( $destination_page ) {
$this->clear_all_handoff_options();
}
}
}
Expand Down
1 change: 1 addition & 0 deletions includes/wizards/class-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public function add_page() {
* Render the container for the wizard.
*/
public function render_wizard() {
do_action( 'newspack_before_wizard_content' );
?>
<div class="newspack-wizard <?php echo esc_attr( $this->slug ); ?>" id="<?php echo esc_attr( $this->slug ); ?>">
</div>
Expand Down
16 changes: 15 additions & 1 deletion packages/components/src/action-card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const ActionCard = ( {
heading = 2,
description,
handoff,
handoffUrl,
bannerText,
bannerButtonText,
editLink,
href,
notification,
Expand Down Expand Up @@ -187,7 +190,18 @@ const ActionCard = ( {
{ actionContent && actionContent }
{ actionText &&
( handoff ? (
<Handoff plugin={ handoff } editLink={ editLink } compact isLink>
<Handoff
plugin={ handoff }
editLink={ editLink }
bannerText={ bannerText }
bannerButtonText={ bannerButtonText }
compact
isLink
>
{ actionText }
</Handoff>
) : handoffUrl ? (
<Handoff url={ handoffUrl } bannerText={ bannerText } bannerButtonText={ bannerButtonText } compact isLink>
{ actionText }
</Handoff>
) : onClick || hasInternalLink ? (
Expand Down
Loading
Loading