diff --git a/assets/css/admin.css b/assets/css/admin.css index 58938bef6..c8862528c 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -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 \*------------------------------------*/ diff --git a/classes/admin/class-sunset-notice.php b/classes/admin/class-sunset-notice.php new file mode 100644 index 000000000..4bcde56ee --- /dev/null +++ b/classes/admin/class-sunset-notice.php @@ -0,0 +1,192 @@ +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 tag, %2$s: closing 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' ), + '', + '' + ); + } + + /** + * 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 tag, %2$s: closing tag. */ + \esc_html__( 'As a keepsake, you can %1$sdownload a certificate%2$s with your site\'s achievements.', 'progress-planner' ), + '', + '' + ); + } + + /** + * 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 ); + ?> + + +
+

get_message() . ' ' . $this->get_certificate_message() ); ?>

+
+ + + 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() ) { diff --git a/tests/phpunit/test-class-sunset-notice.php b/tests/phpunit/test-class-sunset-notice.php new file mode 100644 index 000000000..0c875330c --- /dev/null +++ b/tests/phpunit/test-class-sunset-notice.php @@ -0,0 +1,167 @@ +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 ); + } +} diff --git a/views/sunset-certificate.php b/views/sunset-certificate.php new file mode 100644 index 000000000..47b17c1ec --- /dev/null +++ b/views/sunset-certificate.php @@ -0,0 +1,216 @@ +get_admin__sunset_notice(); +$prpl_completed_badges = $prpl_sunset_notice->get_completed_badges(); +$prpl_activation_date = \progress_planner()->get_activation_date(); +$prpl_badge_svg_url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/badge-svg/?badge_id='; + +// Scale the badges to the available space, so the certificate always fits on a single A4 page. +$prpl_badge_count = \count( $prpl_completed_badges ); +if ( $prpl_badge_count <= 8 ) { + $prpl_badge_size = 24; // mm. +} elseif ( $prpl_badge_count <= 18 ) { + $prpl_badge_size = 18; +} elseif ( $prpl_badge_count <= 32 ) { + $prpl_badge_size = 14; +} else { + $prpl_badge_size = 10; +} + +?> + +> + + + + <?php \esc_html_e( 'Progress Planner certificate', 'progress-planner' ); ?> + + + +
+ + +

+ +

+ ' . \esc_html( \get_bloginfo( 'name' ) ) . '', + '' . \esc_html( \home_url() ) . '' + ); + ?> +

+ +

+ format( 'U' ) ) ) + ); + ?> +

+ + +
+ +
+ <?php echo \esc_attr( $prpl_badge->get_name() ); ?> + get_name() ); ?> +
+ +
+ + + +
+ + + + diff --git a/views/sunset-notice.php b/views/sunset-notice.php new file mode 100644 index 000000000..dedd4d16f --- /dev/null +++ b/views/sunset-notice.php @@ -0,0 +1,22 @@ + +
+
+

+

get_admin__sunset_notice()->get_message() ); ?>

+

+ + + +
+