Skip to content
Draft
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
16 changes: 16 additions & 0 deletions assets/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,22 @@ button.prpl-info-icon {
}
}

/*------------------------------------*\
Sunset notice.
\*------------------------------------*/
.prpl-wrap .prpl-sunset-notice {
border-left: 4px solid var(--prpl-background-banner);

h1 {
color: var(--prpl-color-headings);
margin-top: 0;
}

p {
max-width: 680px;
}
}

/*------------------------------------*\
Buttons
\*------------------------------------*/
Expand Down
192 changes: 192 additions & 0 deletions classes/admin/class-sunset-notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php
/**
* Sunset notice class.
*
* Displays a notice that support for the plugin is ending, on the
* Progress Planner dashboard and on the plugins overview page.
* The notice is hidden when the pp-hosts companion plugin is present.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Admin;

/**
* Sunset notice class.
*/
class Sunset_Notice {

/**
* The URL with more information about the end of support.
*
* @var string
*/
const LEARN_MORE_URL = 'https://prpl.fyi/sunset';

/**
* The admin-post action for the certificate page.
*
* @var string
*/
const CERTIFICATE_ACTION = 'prpl_sunset_certificate';

/**
* Constructor.
*/
public function __construct() {
if ( ! $this->should_show() ) {
return;
}

\add_action( 'progress_planner_admin_page_header_before', [ $this, 'the_dashboard_notice' ] );
\add_action( 'after_plugin_row_' . \plugin_basename( \PROGRESS_PLANNER_FILE ), [ $this, 'the_plugin_row_notice' ] );
\add_action( 'admin_post_' . self::CERTIFICATE_ACTION, [ $this, 'render_certificate' ] );
}

/**
* Whether the sunset notice should be shown.
*
* The notice only applies to installs coming from wordpress.org or FAIR.
* It is hidden when the pp-hosts companion plugin is active (constants or
* class detection), when a host defines a branding ID, or when pp-hosts
* is installed but not (yet) activated.
*
* @return bool
*/
public function should_show() {
$show = ! (
\defined( 'PP_HOSTS_FILE' )
|| \defined( 'PROGRESS_PLANNER_BRANDING_ID' )
|| \class_exists( 'PP_Hosts\Updater' )
|| \progress_planner()->get_plugin_installer()->is_plugin_installed( 'pp-hosts' )
);

/**
* Filter whether the sunset notice should be shown.
*
* @param bool $show Whether the notice should be shown.
*/
return (bool) \apply_filters( 'progress_planner_show_sunset_notice', $show );
}

/**
* Get the notice message, shared by both surfaces.
*
* Contains an anchor tag, so it should be escaped with `wp_kses_post` on output.
*
* @return string
*/
public function get_message() {
return \sprintf(
/* translators: %1$s: opening <a> tag, %2$s: closing </a> tag. */
\esc_html__( 'Support and updates for the free Progress Planner plugin are ending. We are incredibly grateful for your support over the years — thank you for improving your website with us! %1$sLearn more about what this means for your site%2$s.', 'progress-planner' ),
'<a href="' . \esc_url( self::LEARN_MORE_URL ) . '" target="_blank" rel="noopener">',
'</a>'
);
}

/**
* Get the certificate invitation sentence.
*
* Contains an anchor tag, so it should be escaped with `wp_kses_post` on output.
*
* @return string
*/
public function get_certificate_message() {
return \sprintf(
/* translators: %1$s: opening <a> tag, %2$s: closing </a> tag. */
\esc_html__( 'As a keepsake, you can %1$sdownload a certificate%2$s with your site\'s achievements.', 'progress-planner' ),
'<a href="' . \esc_url( $this->get_certificate_url() ) . '" target="_blank" rel="noopener">',
'</a>'
);
}

/**
* Get the URL of the certificate page.
*
* @return string
*/
public function get_certificate_url() {
return \admin_url( 'admin-post.php?action=' . self::CERTIFICATE_ACTION );
}

/**
* Get all completed badges, across all badge contexts.
*
* @return \Progress_Planner\Badges\Badge[]
*/
public function get_completed_badges() {
$completed = [];
foreach ( [ 'content', 'maintenance', 'monthly_flat' ] as $context ) {
foreach ( \progress_planner()->get_badges()->get_badges( $context ) as $badge ) {
$progress = $badge->get_progress();
if ( isset( $progress['progress'] ) && 100 === (int) $progress['progress'] ) {
$completed[] = $badge;
}
}
}
return $completed;
}

/**
* Render the standalone certificate page and exit.
*
* Hooked to admin-post, so this renders outside the WP admin chrome.
*
* @return void
*/
public function render_certificate() {
if ( ! \current_user_can( 'edit_others_posts' ) ) {
\wp_die( \esc_html__( 'You do not have permission to view this page.', 'progress-planner' ) );
}
$this->the_certificate();
exit;
}

/**
* Print the certificate page markup.
*
* @return void
*/
public function the_certificate() {
\progress_planner()->the_view( 'sunset-certificate.php' );
}

/**
* Print the notice banner on the Progress Planner dashboard.
*
* @return void
*/
public function the_dashboard_notice() {
\progress_planner()->the_view( 'sunset-notice.php' );
}

/**
* Print a notice row below the plugin's row on the plugins overview page.
*
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
*
* @return void
*/
public function the_plugin_row_notice( $plugin_file ) {
if ( ! \current_user_can( 'activate_plugins' ) ) {
return;
}

$prpl_colspan = 4;
if ( \function_exists( '_get_list_table' ) ) {
$prpl_colspan = \_get_list_table( 'WP_Plugins_List_Table', [ 'screen' => 'plugins' ] )->get_column_count();
}

$prpl_is_active = \function_exists( 'is_plugin_active' ) && \is_plugin_active( $plugin_file );
?>
<tr class="plugin-update-tr <?php echo \esc_attr( $prpl_is_active ? 'active' : 'inactive' ); ?>" id="prpl-sunset-notice-row" data-slug="progress-planner" data-plugin="<?php echo \esc_attr( $plugin_file ); ?>">
<td colspan="<?php echo (int) $prpl_colspan; ?>" class="plugin-update colspanchange">
<div class="update-message notice inline notice-warning notice-alt">
<p><?php echo \wp_kses_post( $this->get_message() . ' ' . $this->get_certificate_message() ); ?></p>
</div>
</td>
</tr>
<?php
}
}
1 change: 1 addition & 0 deletions classes/class-base.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public function init() {
if ( \is_admin() && \current_user_can( 'edit_others_posts' ) ) {
$this->get_admin__page();
$this->get_admin__tour();
$this->get_admin__sunset_notice();

// Dont add the widget if the privacy policy is not accepted.
if ( true === $this->is_privacy_policy_accepted() ) {
Expand Down
167 changes: 167 additions & 0 deletions tests/phpunit/test-class-sunset-notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
/**
* Class Sunset_Notice_Test
*
* @package Progress_Planner\Tests
*/

namespace Progress_Planner\Tests;

use Progress_Planner\Admin\Sunset_Notice;

/**
* Sunset notice test case.
*/
class Sunset_Notice_Test extends \WP_UnitTestCase {

/**
* Clean up after each test.
*
* @return void
*/
public function tearDown(): void {
\remove_all_filters( 'progress_planner_show_sunset_notice' );
\remove_all_actions( 'progress_planner_admin_page_header_before' );
\remove_all_actions( 'after_plugin_row_' . \plugin_basename( \PROGRESS_PLANNER_FILE ) );
parent::tearDown();
}

/**
* Test that should_show returns true in a clean install.
*
* @return void
*/
public function test_should_show_by_default() {
$notice = new Sunset_Notice();
$this->assertTrue( $notice->should_show() );
}

/**
* Test that hooks are registered on instantiation in a clean install.
*
* @return void
*/
public function test_hooks_registered_by_default() {
$notice = new Sunset_Notice();
$this->assertNotFalse( \has_action( 'progress_planner_admin_page_header_before', [ $notice, 'the_dashboard_notice' ] ) );
$this->assertNotFalse( \has_action( 'after_plugin_row_' . \plugin_basename( \PROGRESS_PLANNER_FILE ), [ $notice, 'the_plugin_row_notice' ] ) );
}

/**
* Test that hooks are not registered when the filter hides the notice.
*
* @return void
*/
public function test_hooks_not_registered_when_filtered_out() {
\add_filter( 'progress_planner_show_sunset_notice', '__return_false' );
$notice = new Sunset_Notice();
$this->assertFalse( $notice->should_show() );
$this->assertFalse( \has_action( 'progress_planner_admin_page_header_before', [ $notice, 'the_dashboard_notice' ] ) );
$this->assertFalse( \has_action( 'after_plugin_row_' . \plugin_basename( \PROGRESS_PLANNER_FILE ), [ $notice, 'the_plugin_row_notice' ] ) );
}

/**
* Test that the message contains the learn-more link.
*
* @return void
*/
public function test_message_contains_learn_more_link() {
$notice = new Sunset_Notice();
$this->assertStringContainsString( Sunset_Notice::LEARN_MORE_URL, $notice->get_message() );
}

/**
* Test the plugin-row notice output.
*
* @return void
*/
public function test_plugin_row_notice_output() {
$user_id = $this->factory->user->create( [ 'role' => 'administrator' ] );
if ( \is_multisite() ) {
\grant_super_admin( $user_id );
}
\wp_set_current_user( $user_id );

$notice = new Sunset_Notice();
\ob_start();
$notice->the_plugin_row_notice( \plugin_basename( \PROGRESS_PLANNER_FILE ), [] );
$output = \ob_get_clean();

$this->assertStringContainsString( 'plugin-update-tr', $output );
$this->assertStringContainsString( 'notice-warning', $output );
$this->assertStringContainsString( Sunset_Notice::LEARN_MORE_URL, $output );
}

/**
* Test that the certificate endpoint is registered on instantiation.
*
* @return void
*/
public function test_certificate_endpoint_registered() {
$notice = new Sunset_Notice();
$this->assertNotFalse( \has_action( 'admin_post_' . Sunset_Notice::CERTIFICATE_ACTION, [ $notice, 'render_certificate' ] ) );
}

/**
* Test that get_completed_badges only returns badges with 100% progress.
*
* @return void
*/
public function test_get_completed_badges_are_complete() {
$notice = new Sunset_Notice();
$badges = $notice->get_completed_badges();
$this->assertIsArray( $badges );
foreach ( $badges as $badge ) {
$this->assertInstanceOf( \Progress_Planner\Badges\Badge::class, $badge );
$this->assertSame( 100, (int) $badge->get_progress()['progress'] );
}
}

/**
* Test the certificate output contains the site URL and activation year.
*
* @return void
*/
public function test_certificate_output() {
$user_id = $this->factory->user->create( [ 'role' => 'administrator' ] );
\wp_set_current_user( $user_id );

$notice = new Sunset_Notice();
\ob_start();
$notice->the_certificate();
$output = \ob_get_clean();

$this->assertStringContainsString( \home_url(), $output );
$this->assertStringContainsString( \progress_planner()->get_activation_date()->format( 'Y' ), $output );
$this->assertStringContainsString( 'prpl-certificate', $output );
}

/**
* Test that the certificate endpoint dies for users without capabilities.
*
* @return void
*/
public function test_certificate_requires_capability() {
\wp_set_current_user( 0 );

$notice = new Sunset_Notice();
$this->expectException( \WPDieException::class );
$notice->render_certificate();
}

/**
* Test that the plugin-row notice outputs nothing for users without capabilities.
*
* @return void
*/
public function test_plugin_row_notice_requires_capability() {
\wp_set_current_user( 0 );

$notice = new Sunset_Notice();
\ob_start();
$notice->the_plugin_row_notice( \plugin_basename( \PROGRESS_PLANNER_FILE ), [] );
$output = \ob_get_clean();

$this->assertSame( '', $output );
}
}
Loading
Loading