Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
19ff397
Add sunset notice with downloadable certificate
tacoverdo Aug 27, 2026
eee87dc
Remove the learn-more link from the sunset notice
tacoverdo Sep 1, 2026
ea055c7
Change notice copy from "are ending" to "have ended"
tacoverdo Sep 1, 2026
d8b1e49
Merge branch 'develop' into taco/sunset-notice
ilicfilip Sep 4, 2026
bec508f
Distinguish the sunset row notice from core update notices
ilicfilip Sep 4, 2026
236db47
Describe every recommendation as a goal, in markdown
ilicfilip Sep 19, 2026
d3f2564
Load markdown recommendations as tasks, for local testing
ilicfilip Sep 19, 2026
8caa120
Make a markdown rule a task provider, not a loose task
ilicfilip Sep 19, 2026
d34dc1f
Require edit_others_posts for the recommendations REST endpoint
ilicfilip Sep 21, 2026
8bdb44b
Fix fatal TypeError on PHP 7.4 in admin submenu position
ilicfilip Sep 21, 2026
3768801
Honour pp_branding_id preview param only for logged-in users
ilicfilip Sep 21, 2026
90fdf0a
Accept array input for the provider REST query params
ilicfilip Sep 21, 2026
8660d8f
Stop regenerating the email completion token on every init
ilicfilip Sep 21, 2026
1f32d9b
Require a page selection when completing "I have this page"
ilicfilip Sep 21, 2026
8bb7d3d
Fix popover context check so "Create this page" opens in the same tab
ilicfilip Sep 21, 2026
2089b0f
Fix monthly badge date window overflowing into the next month
ilicfilip Sep 21, 2026
b511411
Map recommendations CPT capabilities to close non-REST write paths
ilicfilip Sep 22, 2026
5e3db50
Gate auto-onboard and pp_branding_id to administrators
ilicfilip Sep 22, 2026
1effa90
Enforce per-task provider capability on the recommendations REST cont…
ilicfilip Sep 22, 2026
9e10019
Fix multisite provider-caps test and drop duplicated auto-onboard com…
tacoverdo Sep 22, 2026
d6a76d1
Close the create half of the recommendation slug-squat (gap 2)
ilicfilip Sep 22, 2026
30eb5da
Merge pull request #784 from ProgressPlanner/filip/release-1.10.1-aud…
ilicfilip Sep 22, 2026
9bc54e5
Merge branch 'develop' into taco/sunset-notice
ilicfilip Sep 22, 2026
7c18816
Merge pull request #777 from ProgressPlanner/taco/sunset-notice
ilicfilip Sep 22, 2026
d8d2982
Bump version to 1.10.1 and update changelog
ilicfilip Sep 22, 2026
a03361d
Merge remote-tracking branch 'origin/develop' into filip/recommendati…
ilicfilip Sep 23, 2026
a863ff9
Revert the markdown recommendation experiment from develop
ilicfilip Sep 23, 2026
2b0bfc9
Merge pull request #787 from ProgressPlanner/filip/revert-markdown-ex…
ilicfilip Sep 23, 2026
3ccc2bf
Merge branch 'develop' into filip/release-1.10.1
ilicfilip Sep 24, 2026
560a3fe
Merge pull request #786 from ProgressPlanner/filip/release-1.10.1
ilicfilip Sep 24, 2026
b3be01e
Merge branch 'main' into develop
ilicfilip Sep 24, 2026
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
= 1.10.1 =

This release announces the end of support for the free plugin, and ships some stability and hardening improvements.

* The free Progress Planner plugin has reached the end of its support and updates. A notice now appears on the Progress Planner dashboard and the plugins page, and you can download a certificate of appreciation with your site's achievements.

Maintenance:

* Hardened capability and permission checks across the plugin.

Bugs we fixed:

* Fix a fatal error on PHP 7.4 that could affect every wp-admin page.
* Fix the monthly badge date range so activity is counted in the correct month.
* The email-test completion link is no longer invalidated by other page loads.
* The "Set page" tasks now require a page to be selected before completing.
* Fix the "Create this page" link opening in a new tab from within a popover.

= 1.10.0 =

Enhancements:
Expand Down
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
191 changes: 191 additions & 0 deletions classes/admin/class-sunset-notice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?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 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.
*
* @return string
*/
public function get_message() {
return \esc_html__( 'Support and updates for the free Progress Planner plugin have ended. We are incredibly grateful for your support over the years — thank you for improving your website with us!', '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 <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="notice inline notice-warning notice-alt prpl-sunset-row-notice">
<p><?php echo \wp_kses_post( $this->get_message() . ' ' . $this->get_certificate_message() ); ?></p>
</div>
</td>
</tr>
<style>
/* Opt out of core's red "update available" dashicon, which this notice is not. */
#prpl-sunset-notice-row .prpl-sunset-row-notice p:before {
content: "\f534";
display: inline-block;
font: normal 20px/1 dashicons;
color: #b26200;
margin: -3px 5px 0 -2px;
vertical-align: middle;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
<?php
}
}
11 changes: 8 additions & 3 deletions classes/badges/class-monthly.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,16 @@ public function progress_callback( $args = [] ) {
}

$year = $this->get_year();
$month = self::get_months( (int) $year )[ 'm' . $this->get_month() ];
$month_num = (int) $this->get_month();

$start_date = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month_num}-01" );
$end_date = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month_num}-" . \gmdate( 't', \strtotime( $month ) ) );
// Derive the month window from the month number. The previous code used
// gmdate( 't', strtotime( $badge_name ) ), but the badge name (e.g.
// "Felix February") is not a parseable date, so strtotime returned false
// and gmdate( 't', false ) always yielded 31 — overflowing shorter months
// into the next one (Feb -> Mar 3). This matches the monthly-badges
// widget's Y-m-01 / last-day-of-month range.
$start_date = \DateTime::createFromFormat( '!Y-n-j', "{$year}-{$month_num}-1" );
$end_date = ( clone $start_date )->modify( 'last day of this month' );

// Get the activities for the month.
$activities = \progress_planner()->get_activities__query()->query_activities(
Expand Down
15 changes: 13 additions & 2 deletions classes/class-base.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,18 @@ public function init() {
$this->get_utils__playground();
}

$prpl_license_key = $this->get_license_key();
if ( ! $prpl_license_key && 0 !== (int) \progress_planner()->get_ui__branding()->get_branding_id() ) {
// Auto-onboard branded hosts (#633). Require `manage_options` so
// anonymous and lower-privileged visitors (e.g. Subscribers) can't
// trigger the blocking remote calls (1.10.0 audit S3). Not gated on
// `is_admin()`: pp-hosts redirects the admin to the front-end homepage
// after set-up, and this is the only path that fetches the license key.
// The branding check runs before the capability check, so
// `current_user_can()` is only reached on a branded, unlicensed site.
if ( ! \wp_doing_cron()
&& ! $this->get_license_key()
&& 0 !== (int) \progress_planner()->get_ui__branding()->get_branding_id()
&& \current_user_can( 'manage_options' )
) {
$prpl_license_key = \progress_planner()->get_utils__onboard()->make_remote_onboarding_request();
if ( '' !== $prpl_license_key ) {
\update_option( 'progress_planner_license_key', $prpl_license_key, false );
Expand All @@ -110,6 +120,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
18 changes: 18 additions & 0 deletions classes/class-suggested-tasks-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ public function add( $data ) {
$posts = $posts_trashed;
}

// Defense in depth: only treat an existing task as "the task" when its
// provider matches. Otherwise a post that merely squats the slug (e.g.
// one a non-admin created under a different provider) would be treated
// as the task and suppress the real recommendation from being injected.
// See the 1.10.0 audit S1 / review gap 2. Filtered in PHP on the resolved
// provider rather than via a tax_query, because a name + tax_query on a
// trashed post does not reliably AND-combine in WP_Query.
if ( ! empty( $posts ) && ! empty( $data['provider_id'] ) ) {
$posts = \array_values(
\array_filter(
$posts,
static function ( $task ) use ( $data ) {
return $task->get_provider_id() === $data['provider_id'];
}
)
);
}

// If task already exists (in any status), return its ID without creating a duplicate.
if ( ! empty( $posts ) ) {
\delete_option( $lock_key );
Expand Down
Loading
Loading