From 60ec5cfb35e4d01be99f24e5190a5f2049a0fe5a Mon Sep 17 00:00:00 2001 From: marclucraft Date: Tue, 16 Jun 2026 09:50:07 +0100 Subject: [PATCH 1/4] fix: cancel scheduled notification when reverting a post to draft. update version to 3.9.1 --- onesignal.php | 4 +- readme.txt | 5 +- tests/integration/TestAPIIntegration.php | 100 ++++++++++++++++++++++- v3/onesignal-notification.php | 47 +++++++---- 4 files changed, 135 insertions(+), 21 deletions(-) diff --git a/onesignal.php b/onesignal.php index e8c6535..e232fab 100644 --- a/onesignal.php +++ b/onesignal.php @@ -6,7 +6,7 @@ * Plugin Name: OneSignal Push Notifications * Plugin URI: https://onesignal.com/ * Description: Free web push notifications. - * Version: 3.9.0 + * Version: 3.9.1 * Author: OneSignal * Author URI: https://onesignal.com * License: MIT @@ -18,7 +18,7 @@ define('ONESIGNAL_URI_REVEAL_PROJECT_NUMBER', 'reveal_project_number=true'); // Plugin version - must match Version in plugin header -define('ONESIGNAL_PLUGIN_VERSION', '030900'); +define('ONESIGNAL_PLUGIN_VERSION', '030901'); // Constants for plugin versions define('ONESIGNAL_VERSION_V2', 'v2'); diff --git a/readme.txt b/readme.txt index 134a251..299d966 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: push notification, push notifications, desktop notifications, mobile notif Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 3.9.0 +Stable tag: 3.9.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -65,6 +65,9 @@ OneSignal is trusted by over 1.8M+ developers and marketing strategists. We powe == Changelog == += 3.9.1 = +- fix: cancel scheduled OneSignal notification when a scheduled post is reverted to draft or pending + = 3.9.0 = - fix: [SDK-4340] skip enqueueing metabox assets for disallowed post types (#418) - feat: show toast with notification feedback (#417) diff --git a/tests/integration/TestAPIIntegration.php b/tests/integration/TestAPIIntegration.php index c0dc0c9..61777cf 100644 --- a/tests/integration/TestAPIIntegration.php +++ b/tests/integration/TestAPIIntegration.php @@ -47,8 +47,9 @@ protected function setUpContentFiltering() { public function setUp(): void { parent::setUp(); - // Reset HTTP mocks + // Reset HTTP mocks and captured args self::$http_requests_mock = array(); + self::$captured_request_args = array(); global $wp_post_meta, $test_get_option_overrides; $wp_post_meta = array(); @@ -736,6 +737,103 @@ public function test_store_notice_error_status_on_wp_error() { $this->assertSame('Connection timeout', $this->lastTransient['detail']); } + /** + * Test that a pending notification is cancelled when a scheduled post reverts to draft. + */ + public function test_cancel_notification_when_scheduled_post_reverts_to_draft() { + $post_id = 2001; + $notification_id = 'scheduled-notif-to-cancel'; + + global $wp_post_meta; + $wp_post_meta[$post_id]['os_notification_id'] = $notification_id; + $wp_post_meta[$post_id]['os_previous_publish_date'] = '2030-06-01 10:00:00'; + + WP_Mock::userFunction('delete_post_meta') + ->andReturnUsing(function($pid, $meta_key) { + global $wp_post_meta; + unset($wp_post_meta[$pid][$meta_key]); + return true; + }); + + $cancel_url = 'https://onesignal.com/api/v1/notifications/' . $notification_id . '?app_id=test-app-id'; + $this->mock_http_request($cancel_url, [ + 'response' => ['code' => 200], + 'body' => json_encode(['success' => true]), + ]); + + $post = (object) [ + 'ID' => $post_id, + 'post_title' => 'Formerly Scheduled Post', + 'post_type' => 'post', + 'post_date' => '2030-06-01 10:00:00', + 'post_date_gmt' => '2030-06-01 10:00:00', + ]; + + onesignal_schedule_notification('draft', 'future', $post); + + $this->assertArrayHasKey('wp_remote_request', self::$captured_request_args); + $this->assertArrayHasKey($cancel_url, self::$captured_request_args['wp_remote_request']); + $this->assertSame('', onesignal_get_notification_id($post_id)); + $this->assertSame('', get_post_meta($post_id, 'os_previous_publish_date', true)); + } + + /** + * Test that no cancellation is attempted when a scheduled post reverts to draft but has no stored notification ID. + */ + public function test_no_cancellation_when_no_notification_stored_on_draft_revert() { + $post_id = 2002; + + WP_Mock::userFunction('delete_post_meta') + ->andReturnUsing(function($pid, $meta_key) { + global $wp_post_meta; + unset($wp_post_meta[$pid][$meta_key]); + return true; + }); + + $post = (object) [ + 'ID' => $post_id, + 'post_title' => 'No Notification Post', + 'post_type' => 'post', + 'post_date' => '2030-06-01 10:00:00', + 'post_date_gmt' => '2030-06-01 10:00:00', + ]; + + onesignal_schedule_notification('draft', 'future', $post); + + // No HTTP request should have been made + $this->assertArrayNotHasKey('wp_remote_request', self::$captured_request_args); + } + + /** + * Test that no cancellation is attempted for disallowed post types on draft revert. + */ + public function test_no_cancellation_for_disallowed_post_type_on_draft_revert() { + $post_id = 2003; + $notification_id = 'should-not-be-cancelled'; + + global $wp_post_meta; + $wp_post_meta[$post_id]['os_notification_id'] = $notification_id; + + WP_Mock::userFunction('delete_post_meta') + ->andReturnUsing(function($pid, $meta_key) { + global $wp_post_meta; + unset($wp_post_meta[$pid][$meta_key]); + return true; + }); + + $post = (object) [ + 'ID' => $post_id, + 'post_title' => 'Disallowed Type Post', + 'post_type' => 'portfolio', + ]; + + onesignal_schedule_notification('draft', 'future', $post); + + // No HTTP request should have been made; notification ID should remain + $this->assertArrayNotHasKey('wp_remote_request', self::$captured_request_args); + $this->assertSame($notification_id, onesignal_get_notification_id($post_id)); + } + /** * Test store_send_notice stores status='error' and the API error string as detail * when the API returns a non-200 response with a string errors[0]. diff --git a/v3/onesignal-notification.php b/v3/onesignal-notification.php index 282fb14..c32f5fa 100644 --- a/v3/onesignal-notification.php +++ b/v3/onesignal-notification.php @@ -10,7 +10,7 @@ add_action('save_post', 'onesignal_handle_quick_edit_date_change', 10, 3); // Register handler to cancel scheduled notifications when posts are deleted -add_action('wp_trash_post', 'onesignal_cancel_notification_on_post_delete'); +add_action('wp_trash_post', 'onesignal_cancel_and_clear_notification'); // Display admin notices after a notification send attempt (classic editor) add_action('admin_notices', 'onesignal_display_send_notice'); @@ -21,7 +21,9 @@ // Enqueue block editor notice script add_action('enqueue_block_editor_assets', 'onesignal_enqueue_block_editor_notice'); -// Core function to create and send/schedule a notification +/** + * Creates and sends (or schedules) a OneSignal notification for a post. + */ function onesignal_create_notification($post, $notification_options = array()) { $onesignal_wp_settings = get_option("OneSignalWPSetting"); @@ -306,7 +308,9 @@ function onesignal_enqueue_block_editor_notice() )); } -// Function to schedule notification (called on post status transitions) +/** + * Fires on every post status transition; sends or cancels notifications as appropriate. + */ function onesignal_schedule_notification($new_status, $old_status, $post) { if (($new_status === 'publish') || ($new_status === 'future')) { @@ -328,10 +332,20 @@ function onesignal_schedule_notification($new_status, $old_status, $post) // Call the core notification function onesignal_create_notification($post, $notification_options); + } elseif ($old_status === 'future' && $new_status !== 'publish' && $new_status !== 'future') { + // A scheduled post was reverted to a non-scheduled status (e.g. draft, pending). + // Cancel the pending OneSignal notification so it isn't delivered at the old scheduled time. + if (!onesignal_is_post_type_allowed($post->post_type)) { + return; + } + + onesignal_cancel_and_clear_notification($post->ID); } } -// Function to handle quick-edit publish date changes +/** + * Cancels and re-schedules a notification when a scheduled post's publish date is changed via quick-edit. + */ function onesignal_handle_quick_edit_date_change($post_id, $post, $update) { // Check user capability to edit this post @@ -405,21 +419,20 @@ function onesignal_handle_quick_edit_date_change($post_id, $post, $update) } } -// Function to cancel scheduled notifications when a post is deleted -function onesignal_cancel_notification_on_post_delete($post_id) +/** + * Cancels the stored OneSignal notification, and clears its meta, on post deletion or status change (e.g. scheduled -> draft). + */ +function onesignal_cancel_and_clear_notification($post_id) { - $post = get_post($post_id); - - if (!$post) { - return; + $existing_notification_id = onesignal_get_notification_id($post_id); + if (empty($existing_notification_id)) { + return false; } - $existing_notification_id = onesignal_get_notification_id($post_id); - if (!empty($existing_notification_id)) { - $cancelled = onesignal_cancel_notification($existing_notification_id); - if ($cancelled) { - delete_post_meta($post_id, 'os_notification_id'); - delete_post_meta($post_id, 'os_previous_publish_date'); - } + $cancelled = onesignal_cancel_notification($existing_notification_id); + if ($cancelled) { + delete_post_meta($post_id, 'os_notification_id'); + delete_post_meta($post_id, 'os_previous_publish_date'); } + return $cancelled; } From abb4dbd40a28680e63dae34022147f80c2ca3dde Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 24 Jun 2026 13:06:11 -0700 Subject: [PATCH 2/4] test: update wp_trash_post hook assertions to renamed onesignal_cancel_and_clear_notification The handler was renamed from onesignal_cancel_notification_on_post_delete but the hook-registration test still referenced the old name, making the assertion self-referential and decoupled from the production registration. --- tests/integration/TestPostHooks.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/TestPostHooks.php b/tests/integration/TestPostHooks.php index 52aac58..d9b8efb 100644 --- a/tests/integration/TestPostHooks.php +++ b/tests/integration/TestPostHooks.php @@ -192,7 +192,7 @@ public function setUp(): void { 'accepted_args' => 3 ); $wp_filters['wp_trash_post'][10][] = array( - 'function' => 'onesignal_cancel_notification_on_post_delete', + 'function' => 'onesignal_cancel_and_clear_notification', 'accepted_args' => 1 ); } @@ -215,7 +215,7 @@ public function test_save_post_hook_registered() { * Test that wp_trash_post hook is registered */ public function test_wp_trash_post_hook_registered() { - $this->assertNotFalse(has_action('wp_trash_post', 'onesignal_cancel_notification_on_post_delete')); + $this->assertNotFalse(has_action('wp_trash_post', 'onesignal_cancel_and_clear_notification')); } /** From 588c7e9fe08cfa31f6aabce01c21a9ab4c4e837b Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 24 Jun 2026 13:06:25 -0700 Subject: [PATCH 3/4] refactor: drop redundant status guards in scheduled-notification revert branch The elseif already follows the publish/future branch, so new_status can never be publish or future here; the extra checks were dead. --- v3/onesignal-notification.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/onesignal-notification.php b/v3/onesignal-notification.php index c32f5fa..1eef7aa 100644 --- a/v3/onesignal-notification.php +++ b/v3/onesignal-notification.php @@ -332,7 +332,7 @@ function onesignal_schedule_notification($new_status, $old_status, $post) // Call the core notification function onesignal_create_notification($post, $notification_options); - } elseif ($old_status === 'future' && $new_status !== 'publish' && $new_status !== 'future') { + } elseif ($old_status === 'future') { // A scheduled post was reverted to a non-scheduled status (e.g. draft, pending). // Cancel the pending OneSignal notification so it isn't delivered at the old scheduled time. if (!onesignal_is_post_type_allowed($post->post_type)) { From 2caf47e457b77cac3ad4316719e32f3bfcb80327 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 24 Jun 2026 13:06:47 -0700 Subject: [PATCH 4/4] chore: revert 3.9.1 version bump and changelog Keep version metadata and changelog updates to a dedicated release PR. --- onesignal.php | 4 ++-- readme.txt | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/onesignal.php b/onesignal.php index e232fab..e8c6535 100644 --- a/onesignal.php +++ b/onesignal.php @@ -6,7 +6,7 @@ * Plugin Name: OneSignal Push Notifications * Plugin URI: https://onesignal.com/ * Description: Free web push notifications. - * Version: 3.9.1 + * Version: 3.9.0 * Author: OneSignal * Author URI: https://onesignal.com * License: MIT @@ -18,7 +18,7 @@ define('ONESIGNAL_URI_REVEAL_PROJECT_NUMBER', 'reveal_project_number=true'); // Plugin version - must match Version in plugin header -define('ONESIGNAL_PLUGIN_VERSION', '030901'); +define('ONESIGNAL_PLUGIN_VERSION', '030900'); // Constants for plugin versions define('ONESIGNAL_VERSION_V2', 'v2'); diff --git a/readme.txt b/readme.txt index 299d966..134a251 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: push notification, push notifications, desktop notifications, mobile notif Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 3.9.1 +Stable tag: 3.9.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -65,9 +65,6 @@ OneSignal is trusted by over 1.8M+ developers and marketing strategists. We powe == Changelog == -= 3.9.1 = -- fix: cancel scheduled OneSignal notification when a scheduled post is reverted to draft or pending - = 3.9.0 = - fix: [SDK-4340] skip enqueueing metabox assets for disallowed post types (#418) - feat: show toast with notification feedback (#417)