diff --git a/assets/css/page-widgets/todo.css b/assets/css/page-widgets/todo.css index cdd6898d96..1c7745fa5b 100644 --- a/assets/css/page-widgets/todo.css +++ b/assets/css/page-widgets/todo.css @@ -39,7 +39,7 @@ display: inline-block; width: 24px; height: 24px; - background-image: url("../../images/icon_progress_planner.svg"); + background-image: var(--prpl-icon-url); background-size: contain; background-repeat: no-repeat; } @@ -136,7 +136,7 @@ display: inline-block; width: 24px; height: 24px; - background-image: url("../../images/icon_progress_planner.svg"); + background-image: var(--prpl-icon-url); background-size: contain; background-repeat: no-repeat; } diff --git a/assets/js/focus-element.js b/assets/js/focus-element.js index 09b56d2e02..174b38b77f 100644 --- a/assets/js/focus-element.js +++ b/assets/js/focus-element.js @@ -8,9 +8,7 @@ const prplGetIndicatorElement = ( content, taskId, points ) => { // Create an element. const imgEl = document.createElement( 'img' ); - imgEl.src = - progressPlannerFocusElement.base_url + - '/assets/images/icon_progress_planner.svg'; + imgEl.src = progressPlannerFocusElement.iconUrl; imgEl.alt = points ? prplL10n( 'fixThisIssue' ).replace( '%d', points ) : ''; diff --git a/assets/js/yoast-focus-element.js b/assets/js/yoast-focus-element.js index e35cda92e7..f5b1b01c13 100644 --- a/assets/js/yoast-focus-element.js +++ b/assets/js/yoast-focus-element.js @@ -14,7 +14,7 @@ class ProgressPlannerYoastFocus { constructor() { this.container = document.querySelector( '#yoast-seo-settings' ); this.tasks = progressPlannerYoastFocusElement.tasks; - this.baseUrl = progressPlannerYoastFocusElement.base_url; + this.iconUrl = progressPlannerYoastFocusElement.iconUrl; if ( this.container ) { this.init(); @@ -219,7 +219,7 @@ class ProgressPlannerYoastFocus { // Create an icon image. const iconImg = document.createElement( 'img' ); - iconImg.src = this.baseUrl + '/assets/images/icon_progress_planner.svg'; + iconImg.src = this.iconUrl; iconImg.alt = 'Ravi'; iconImg.width = 16; iconImg.height = 16; diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 8959089943..5908b9850f 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -301,7 +301,7 @@ public function localize_script( $handle, $localize_data = [] ) { $localize_data = [ 'name' => 'prplCelebrate', 'data' => [ - 'raviIconUrl' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg', + 'raviIconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(), 'confettiOptions' => $confetti_options, ], ]; diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php index 21c58a8647..73d38c170f 100644 --- a/classes/admin/class-page.php +++ b/classes/admin/class-page.php @@ -261,7 +261,7 @@ public function maybe_enqueue_focus_el_script( $hook ) { 'tasks' => $tasks_details, 'totalPoints' => $total_points, 'completedPoints' => $completed_points, - 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ), + 'iconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(), 'l10n' => [ /* translators: %d: The number of points. */ 'fixThisIssue' => \esc_html__( 'Fix this issue to get %d point(s) in Progress Planner', 'progress-planner' ), diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php index 171abb3d0a..bfc2903d2a 100644 --- a/classes/admin/widgets/class-todo.php +++ b/classes/admin/widgets/class-todo.php @@ -26,6 +26,19 @@ final class ToDo extends Widget { */ protected $width = 2; + /** + * Enqueue styles. + * + * @return void + */ + public function enqueue_styles() { + parent::enqueue_styles(); + \wp_add_inline_style( + "progress-planner/page-widgets/{$this->id}", + ':root { --prpl-icon-url: url("' . \progress_planner()->get_ui__branding()->get_admin_menu_icon() . '"); }' + ); + } + /** * Print the widget content. * diff --git a/classes/class-badges.php b/classes/class-badges.php index 904c94f0b2..2c751ba636 100644 --- a/classes/class-badges.php +++ b/classes/class-badges.php @@ -83,7 +83,19 @@ public function __construct() { * @return \Progress_Planner\Badges\Badge[] */ public function get_badges( $context ) { - return isset( $this->$context ) ? $this->$context : []; + // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline @var for PHPStan. + /** @var \Progress_Planner\Badges\Badge[] $badges */ + $badges = isset( $this->$context ) ? $this->$context : []; + + /** + * Filter the badges for a context. + * + * @param \Progress_Planner\Badges\Badge[] $badges The badges. + * @param string $context The badges context. + * + * @return \Progress_Planner\Badges\Badge[] + */ + return \apply_filters( 'progress_planner_badges', $badges, $context ); } /** @@ -101,7 +113,18 @@ public function get_badge( $badge_id ) { } } } - return null; + + /** + * Filter for retrieving a single badge by ID. + * + * Allows external plugins to provide custom badges. + * + * @param \Progress_Planner\Badges\Badge|null $badge The badge object or null. + * @param string $badge_id The badge ID. + * + * @return \Progress_Planner\Badges\Badge|null + */ + return \apply_filters( 'progress_planner_get_badge', null, $badge_id ); } /** @@ -183,6 +206,8 @@ public function get_latest_completed_badge() { // Get the settings for badges (stores completion dates). $settings = \progress_planner()->get_settings()->get( 'badges', [] ); + // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline @var for PHPStan. + /** @var string|null $latest_date */ $latest_date = null; // Loop through all badge contexts to find the most recently completed badge. @@ -204,20 +229,30 @@ public function get_latest_completed_badge() { if ( null === $latest_date ) { $this->latest_completed_badge = $badge; if ( isset( $settings[ $badge->get_id() ]['date'] ) ) { - $latest_date = $settings[ $badge->get_id() ]['date']; + $latest_date = (string) $settings[ $badge->get_id() ]['date']; } continue; } // Compare completion dates as Unix timestamps to find the most recent. // Using >= ensures that if multiple badges complete simultaneously, the last one processed wins. - if ( \DateTime::createFromFormat( 'Y-m-d H:i:s', $settings[ $badge->get_id() ]['date'] )->format( 'U' ) >= \DateTime::createFromFormat( 'Y-m-d H:i:s', $latest_date )->format( 'U' ) ) { - $latest_date = $settings[ $badge->get_id() ]['date']; + if ( \DateTime::createFromFormat( 'Y-m-d H:i:s', (string) $settings[ $badge->get_id() ]['date'] )->format( 'U' ) >= \DateTime::createFromFormat( 'Y-m-d H:i:s', $latest_date )->format( 'U' ) ) { + $latest_date = (string) $settings[ $badge->get_id() ]['date']; $this->latest_completed_badge = $badge; } } } + /** + * Filter the latest completed badge. + * + * @param \Progress_Planner\Badges\Badge|null $badge The latest completed badge. + * @param string|null $latest_date The latest completion date. + * + * @return \Progress_Planner\Badges\Badge|null + */ + $this->latest_completed_badge = \apply_filters( 'progress_planner_latest_completed_badge', $this->latest_completed_badge, $latest_date ); + return $this->latest_completed_badge; } } diff --git a/classes/class-lessons.php b/classes/class-lessons.php index c9efdecae4..2373395d0c 100644 --- a/classes/class-lessons.php +++ b/classes/class-lessons.php @@ -58,7 +58,7 @@ public function get_remote_api_items() { [ 'site' => \get_site_url(), 'license_key' => \progress_planner()->get_license_key(), - 'locale' => apply_filters( 'prpl_lesson_locale', \get_locale() ), + 'locale' => \apply_filters( 'prpl_lesson_locale', \get_locale() ), ], $url ); diff --git a/classes/class-plugin-upgrade-tasks.php b/classes/class-plugin-upgrade-tasks.php index a111e79065..f1762c9d68 100644 --- a/classes/class-plugin-upgrade-tasks.php +++ b/classes/class-plugin-upgrade-tasks.php @@ -104,14 +104,26 @@ protected function add_initial_onboarding_tasks() { public function maybe_add_onboarding_tasks() { $onboard_task_provider_ids = \apply_filters( 'prpl_onboarding_task_providers', [] ); - // Privacy policy is not accepted, so it's a fresh install. - $fresh_install = ! \progress_planner()->is_privacy_policy_accepted(); - - // Check if task providers option exists, it will not on fresh installs and v1.0.4 and older. - $old_task_providers = \get_option( 'progress_planner_previous_version_task_providers', [] ); + // Check if task providers option exists. If not, it's either a fresh install or upgrading from v1.0.4 or older. + $old_task_providers = \get_option( 'progress_planner_previous_version_task_providers', [] ); + $task_providers_option_set = false !== \get_option( 'progress_planner_previous_version_task_providers', false ); + + // Fresh install detection: + // - Option doesn't exist AND privacy policy not yet accepted (standalone fresh install) + // - Option doesn't exist AND running as branded/hosted version (pp-hosts fresh install, privacy auto-accepted) + // In these cases, save current providers as baseline without showing upgrade popover. + if ( ! $task_providers_option_set ) { + $is_branded_version = \defined( 'PROGRESS_PLANNER_BRANDING_ID' ); + $is_privacy_policy_pending = ! \progress_planner()->is_privacy_policy_accepted(); + + if ( $is_branded_version || $is_privacy_policy_pending ) { + // Fresh install - save current providers as baseline and skip upgrade popover. + \update_option( 'progress_planner_previous_version_task_providers', \array_unique( $onboard_task_provider_ids ), SORT_REGULAR ); + return; + } - // We're upgrading from v1.0.4 or older, set the old task providers to what we had before the upgrade. - if ( ! $fresh_install && empty( $old_task_providers ) ) { + // Upgrading from v1.0.4 or older (standalone, privacy accepted but no task providers option). + // Set baseline to what existed before the upgrade. $old_task_providers = [ 'core-blogdescription', 'wp-debug-display', diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index e25fc60367..d88ff5dc7b 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -100,12 +100,25 @@ public function init(): void { * @return void */ public function insert_activity( string $task_id ): void { + /** + * Filter the activity category for a completed task. + * + * Allows customizing the category used when recording task completion activities. + * For example, onboarding tasks may use 'onboarding_task' instead of 'suggested_task' + * to exclude them from monthly badge calculations. + * + * @param string $category The activity category (default: 'suggested_task'). + * @param string $task_id The task ID being completed. + */ + $category = \apply_filters( 'progress_planner_task_activity_category', 'suggested_task', $task_id ); + // Insert an activity. - $activity = new Suggested_Task_Activity(); - $activity->type = 'completed'; - $activity->data_id = (string) $task_id; - $activity->date = new \DateTime(); - $activity->user_id = \get_current_user_id(); + $activity = new Suggested_Task_Activity(); + $activity->category = $category; + $activity->type = 'completed'; + $activity->data_id = (string) $task_id; + $activity->date = new \DateTime(); + $activity->user_id = \get_current_user_id(); $activity->save(); // Allow other classes to react to the completion of a suggested task. diff --git a/classes/suggested-tasks/providers/class-core-update.php b/classes/suggested-tasks/providers/class-core-update.php index 0a681b4373..63f00960eb 100644 --- a/classes/suggested-tasks/providers/class-core-update.php +++ b/classes/suggested-tasks/providers/class-core-update.php @@ -89,7 +89,7 @@ public function add_core_update_link( $update_actions ) { foreach ( \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ) as $task ) { if ( $this->get_task_id() === \progress_planner()->get_suggested_tasks()->get_task_id_from_slug( $task->post_name ) ) { $update_actions['prpl_core_update'] = - 'Progress Planner' . + 'Progress Planner' . '' . \esc_html__( 'Click here to celebrate your completed task!', 'progress-planner' ) . ''; break; } diff --git a/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php b/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php index 6df0b6f0c1..efc0f5ed1d 100644 --- a/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php +++ b/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php @@ -151,8 +151,8 @@ protected function is_plugin_active() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugins = get_plugins(); - $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && is_plugin_active( $this->plugin_path ); + $plugins = \get_plugins(); + $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && \is_plugin_active( $this->plugin_path ); } return $this->is_plugin_active; @@ -168,7 +168,7 @@ protected function get_autoloaded_options_count() { if ( null === $this->autoloaded_options_count ) { $autoload_values = \wp_autoload_values_to_autoload(); - $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); + $placeholders = \implode( ',', \array_fill( 0, \count( $autoload_values ), '%s' ) ); // phpcs:disable WordPress.DB $this->autoloaded_options_count = $wpdb->get_var( diff --git a/classes/suggested-tasks/providers/class-set-valuable-post-types.php b/classes/suggested-tasks/providers/class-set-valuable-post-types.php index 35d4f30ae4..dc522fb43a 100644 --- a/classes/suggested-tasks/providers/class-set-valuable-post-types.php +++ b/classes/suggested-tasks/providers/class-set-valuable-post-types.php @@ -81,7 +81,7 @@ public function check_public_post_types() { \update_option( 'progress_planner_public_post_types', $public_post_types ); // Exit if post type was removed, or it is not public anymore, since the user will not to able to make different selection. - if ( count( $public_post_types ) < count( $previosly_set_public_post_types ) ) { + if ( \count( $public_post_types ) < \count( $previosly_set_public_post_types ) ) { return; } @@ -180,8 +180,8 @@ public function handle_interactive_task_specific_submit() { } $post_types = \wp_unslash( $_POST['prpl-post-types-include'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- array elements are sanitized below. - $post_types = explode( ',', $post_types ); - $post_types = array_map( 'sanitize_text_field', $post_types ); + $post_types = \explode( ',', $post_types ); + $post_types = \array_map( 'sanitize_text_field', $post_types ); \progress_planner()->get_admin__page_settings()->save_post_types( $post_types ); diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php index c4a96db060..1eeb1f91e5 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php @@ -66,8 +66,8 @@ public function enqueue_assets( $hook ) { [ 'name' => 'progressPlannerYoastFocusElement', 'data' => [ - 'tasks' => $focus_tasks, - 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ), + 'tasks' => $focus_tasks, + 'iconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(), ], ] ); diff --git a/progress-planner.php b/progress-planner.php index 49218aba2f..5fa2c26a6a 100644 --- a/progress-planner.php +++ b/progress-planner.php @@ -28,7 +28,7 @@ require_once PROGRESS_PLANNER_DIR . '/autoload.php'; -if ( ! function_exists( 'progress_planner' ) ) { +if ( ! \function_exists( 'progress_planner' ) ) { /** * Get the progress planner instance. * diff --git a/tests/phpunit/test-class-base.php b/tests/phpunit/test-class-base.php index 1dfc8f5ae3..a7c9d01051 100644 --- a/tests/phpunit/test-class-base.php +++ b/tests/phpunit/test-class-base.php @@ -215,7 +215,7 @@ public function test_is_debug_mode_enabled() { $this->assertFalse( $result ); // Set the current user to a user with the manage_options capability. - wp_set_current_user( 1 ); + \wp_set_current_user( 1 ); // Option is set, and the user has the manage_options capability. $result = $this->base_instance->is_debug_mode_enabled(); @@ -228,7 +228,7 @@ public function test_is_debug_mode_enabled() { $this->assertIsBool( $result ); // Unset the current user. - wp_set_current_user( 0 ); + \wp_set_current_user( 0 ); } } diff --git a/tests/phpunit/test-class-page-types.php b/tests/phpunit/test-class-page-types.php index eac50f96d0..becd2c9625 100644 --- a/tests/phpunit/test-class-page-types.php +++ b/tests/phpunit/test-class-page-types.php @@ -74,7 +74,7 @@ public static function set_lessons_cache() { [ 'site' => \get_site_url(), 'license_key' => \progress_planner()->get_license_key(), - 'locale' => apply_filters( 'prpl_lesson_locale', \get_locale() ), + 'locale' => \apply_filters( 'prpl_lesson_locale', \get_locale() ), ], $url ); diff --git a/tests/phpunit/traits/test-class-ajax-security-aioseo.php b/tests/phpunit/traits/test-class-ajax-security-aioseo.php index fb37ca13c2..b6a27d5db4 100644 --- a/tests/phpunit/traits/test-class-ajax-security-aioseo.php +++ b/tests/phpunit/traits/test-class-ajax-security-aioseo.php @@ -82,7 +82,7 @@ public function test_verify_aioseo_active_or_fail_not_active() { } catch ( \WPAjaxDieContinueException $e ) { // WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer. // Get the response. - $response = json_decode( $this->_last_response, true ); + $response = \json_decode( $this->_last_response, true ); $this->assertFalse( $response['success'] ); $this->assertArrayHasKey( 'data', $response ); $this->assertArrayHasKey( 'message', $response['data'] ); diff --git a/tests/phpunit/traits/test-class-ajax-security-base.php b/tests/phpunit/traits/test-class-ajax-security-base.php index 554a308cc0..d7b91f5045 100644 --- a/tests/phpunit/traits/test-class-ajax-security-base.php +++ b/tests/phpunit/traits/test-class-ajax-security-base.php @@ -115,7 +115,7 @@ public function test_verify_nonce_or_fail_invalid() { } catch ( \WPAjaxDieContinueException $e ) { // WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer. // Get the response. - $response = json_decode( $this->_last_response, true ); + $response = \json_decode( $this->_last_response, true ); $this->assertFalse( $response['success'] ); $this->assertArrayHasKey( 'data', $response ); $this->assertArrayHasKey( 'message', $response['data'] ); @@ -168,7 +168,7 @@ public function test_verify_capability_or_fail_non_admin() { } catch ( \WPAjaxDieContinueException $e ) { // WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer. // Get the response. - $response = json_decode( $this->_last_response, true ); + $response = \json_decode( $this->_last_response, true ); $this->assertFalse( $response['success'] ); $this->assertArrayHasKey( 'data', $response ); $this->assertArrayHasKey( 'message', $response['data'] ); diff --git a/tests/phpunit/traits/test-class-ajax-security-yoast.php b/tests/phpunit/traits/test-class-ajax-security-yoast.php index 07c775a132..9bcda9724e 100644 --- a/tests/phpunit/traits/test-class-ajax-security-yoast.php +++ b/tests/phpunit/traits/test-class-ajax-security-yoast.php @@ -82,7 +82,7 @@ public function test_verify_yoast_active_or_fail_not_active() { } catch ( \WPAjaxDieContinueException $e ) { // WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer. // Get the response. - $response = json_decode( $this->_last_response, true ); + $response = \json_decode( $this->_last_response, true ); $this->assertFalse( $response['success'] ); $this->assertArrayHasKey( 'data', $response ); $this->assertArrayHasKey( 'message', $response['data'] ); diff --git a/views/dashboard-widgets/score.php b/views/dashboard-widgets/score.php index 5908b4951d..da80254c82 100644 --- a/views/dashboard-widgets/score.php +++ b/views/dashboard-widgets/score.php @@ -69,7 +69,7 @@ + + diff --git a/views/welcome.php b/views/welcome.php index 5a55a3b742..1e781e5708 100644 --- a/views/welcome.php +++ b/views/welcome.php @@ -25,7 +25,7 @@

- the_asset( 'images/icon_progress_planner.svg' ); ?> + get_ui__branding()->get_admin_menu_icon( true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SVG markup. ?>