From bdf7c191b17f61812987735bd6a297b9f8b6fa2c Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 7 Jan 2026 16:44:36 +0000 Subject: [PATCH 01/10] test: add integration test for revision nonce handling in notifications --- .../NotificationsClassicEditorTest.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/Integration/NotificationsClassicEditorTest.php b/tests/Integration/NotificationsClassicEditorTest.php index 0d23ff62..d25e403c 100644 --- a/tests/Integration/NotificationsClassicEditorTest.php +++ b/tests/Integration/NotificationsClassicEditorTest.php @@ -121,6 +121,67 @@ function () use ( &$wp_die_called, &$wp_die_message ) { ); } + /** + * Test that saving a post via wp_update_post does not fail due to revision nonce mismatch. + * + * When WordPress saves a post, it creates a revision which triggers transition_post_status + * with a different post ID (the revision ID). The nonce was created for the original post, + * so if save_post_subscriptions doesn't skip revisions, the nonce check will fail. + * + * @ticket https://wordpress.org/support/topic/upgrading-to-0-10-0-breaks-funtionality-for-editor-role/ + */ + public function test_save_post_with_revision_does_not_fail_nonce_check() { + // Create a post. + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + // Simulate Classic Editor POST request with a valid nonce for the original post. + $_POST['_wpnonce'] = wp_create_nonce( 'update-post_' . $post_id ); + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + // Track if wp_die was called. + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + // Update the post which triggers the full save lifecycle including revision creation. + // This will fire transition_post_status for both the post AND the revision. + // The revision has a different ID, so without the revision skip fix, nonce check fails. + $exception_thrown = false; + try { + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Updated content to trigger revision', + ) + ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'wp_update_post should not trigger wp_die - save_post_subscriptions must skip revisions to avoid nonce mismatch' + ); + $this->assertFalse( + $exception_thrown, + 'wp_update_post threw an exception due to wp_die being called during revision save' + ); + } + /** * Test that the nonce verification uses the correct action string. * From 5ebf65ace2d56d08a121d2a762ea67d5f3744d6d Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 7 Jan 2026 16:46:26 +0000 Subject: [PATCH 02/10] docs: move Development section from README to CONTRIBUTING --- CONTRIBUTING.md | 37 +++++++++++++++++++++++++++++++++++++ README.md | 36 ------------------------------------ 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3a14048..9166c748 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,43 @@ Here's a sample of what a great summary looks like: Screenshots: *screenshot of behavior/error goes here* +Development Setup +------ + +This plugin uses `wp-env` for development and is required to run the tests written for the plugin. `wp-env` requires Docker so please ensure you have that installed on your system first. To install `wp-env`, use the following command: + +``` +npm -g i @wordpress/env +``` + +Read more about `wp-env` [here](https://www.npmjs.com/package/@wordpress/env). + +This plugin also uses Composer to manage PHP dependencies. Composer can be downloaded [here](https://getcomposer.org/download/). + +###### Getting started + +1. Clone the plugin repo: `git clone git@github.com:Automattic/Edit-Flow.git` +2. Change to cloned directory: `cd /path/to/repo` +3. Install PHP dependencies: `composer install` +4. Install NPM dependencies: `npm install` +5. Start dev environment: `wp-env start` + +###### Running tests + +Ensure that the dev environment has already been started with `wp-env start`. + +**PHP Integration Tests:** +1. Integration test: `composer run integration` +2. Multi-site integration test: `composer run integration-ms` + +**E2E Tests (Playwright):** +1. Run E2E tests: `npm run test-e2e` +2. Run with visible browser: `npm run test-e2e:headed` +3. Debug mode: `npm run test-e2e:debug` + +**JavaScript Tests:** +1. Run Jest tests: `npm run test-jest` + Creating and submitting Patches ------ diff --git a/README.md b/README.md index f9f2a6b7..bae753e2 100644 --- a/README.md +++ b/README.md @@ -36,42 +36,6 @@ If the automatic process above fails, follow these simple steps to do a manual i 2. Activate the plugin through the 'Plugins' menu in WordPress 3. Write and enjoy the merits of a structured editorial workflow! -## Development - -This plugin uses `wp-env` for development and is required to run the tests written for the plugin. `wp-env` requires Docker so please ensure you have that installed on your system first. To install `wp-env`, use the following command: - -``` -npm -g i @wordpress/env -``` - -Read more about `wp-env` [here](https://www.npmjs.com/package/@wordpress/env). - -This plugin also uses Composer to manage PHP dependencies. Composer can be downloaded [here](https://getcomposer.org/download/). - -### Getting started - -1. Clone the plugin repo: `git clone git@github.com:Automattic/Edit-Flow.git` -2. Changed to cloned directory: `cd /path/to/repo` -3. Install PHP dependencies: `composer install` -4. Install NPM dependencies: `npm install` -5. Start dev environment: `wp-env start` - -### Running tests - -Ensure that the dev environment has already been started with `wp-env start`. - -**PHP Integration Tests:** -1. Integration test: `composer run integration` -2. Multi-site integration test: `composer run integration-ms` - -**E2E Tests (Playwright):** -1. Run E2E tests: `npm run test-e2e` -2. Run with visible browser: `npm run test-e2e:headed` -3. Debug mode: `npm run test-e2e:debug` - -**JavaScript Tests:** -1. Run Jest tests: `npm run test-jest` - ## Frequently Asked Questions ### Does Edit Flow work with multisite? From efb3f95b0f9429b3c4ae4a4ad6d3206b9398ccf8 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 7 Jan 2026 16:55:54 +0000 Subject: [PATCH 03/10] ci: add path filters to skip tests for irrelevant changes --- .github/workflows/e2e-and-js-tests.yml | 33 ++++++++++++++++++++++---- .github/workflows/integration.yml | 21 ++++++++++++---- .github/workflows/php-lint.yml | 19 +++++++++++---- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-and-js-tests.yml b/.github/workflows/e2e-and-js-tests.yml index ba3a1f17..44e0b657 100644 --- a/.github/workflows/e2e-and-js-tests.yml +++ b/.github/workflows/e2e-and-js-tests.yml @@ -1,11 +1,36 @@ name: E2E and JS tests on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - '**.scss' + - '**.css' + - 'package.json' + - 'package-lock.json' + - 'webpack.config.js' + - '.wp-env.json' + - 'tests/e2e/**' + - '.github/workflows/e2e-and-js-tests.yml' + pull_request: + branches: [develop, main] + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - '**.scss' + - '**.css' + - 'package.json' + - 'package-lock.json' + - 'webpack.config.js' + - '.wp-env.json' + - 'tests/e2e/**' + - '.github/workflows/e2e-and-js-tests.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 57480671..67a804a7 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,11 +1,24 @@ name: Integration Tests on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.wp-env.json' + - '.github/workflows/integration.yml' + pull_request: + branches: [develop, main] + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.wp-env.json' + - '.github/workflows/integration.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml index c609c23b..ac1ded3e 100644 --- a/.github/workflows/php-lint.yml +++ b/.github/workflows/php-lint.yml @@ -1,11 +1,22 @@ name: PHP Lint on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - '.github/workflows/php-lint.yml' + pull_request: + branches: [develop, main] + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - '.github/workflows/php-lint.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} From 5aa5ac51bdb362cb477e92de240a957ad48f0403 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 7 Jan 2026 17:25:43 +0000 Subject: [PATCH 04/10] ci: split JS tests and E2E tests into separate workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jest unit tests don't need wp-env, so separating them provides faster feedback for pure JS changes. E2E tests still require the full WordPress environment with Playwright. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../{e2e-and-js-tests.yml => e2e-tests.yml} | 17 +++--- .github/workflows/js-tests.yml | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) rename .github/workflows/{e2e-and-js-tests.yml => e2e-tests.yml} (92%) create mode 100644 .github/workflows/js-tests.yml diff --git a/.github/workflows/e2e-and-js-tests.yml b/.github/workflows/e2e-tests.yml similarity index 92% rename from .github/workflows/e2e-and-js-tests.yml rename to .github/workflows/e2e-tests.yml index 44e0b657..ca78870c 100644 --- a/.github/workflows/e2e-and-js-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,4 +1,4 @@ -name: E2E and JS tests +name: E2E Tests on: push: @@ -14,7 +14,7 @@ on: - 'webpack.config.js' - '.wp-env.json' - 'tests/e2e/**' - - '.github/workflows/e2e-and-js-tests.yml' + - '.github/workflows/e2e-tests.yml' pull_request: branches: [develop, main] paths: @@ -29,7 +29,7 @@ on: - 'webpack.config.js' - '.wp-env.json' - 'tests/e2e/**' - - '.github/workflows/e2e-and-js-tests.yml' + - '.github/workflows/e2e-tests.yml' workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job @@ -41,7 +41,7 @@ concurrency: jobs: build: - name: Build and Lint + name: Build runs-on: ubuntu-latest permissions: contents: read @@ -85,9 +85,6 @@ jobs: echo "| custom-status-block.js | $(du -h build/custom-status-block.js | cut -f1) |" >> $GITHUB_STEP_SUMMARY echo "| calendar-react.js | $(du -h build/calendar-react.js | cut -f1) |" >> $GITHUB_STEP_SUMMARY - - name: Run Lint JS - run: npm run lint-js - - name: Upload build artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: @@ -96,7 +93,7 @@ jobs: retention-days: 1 test: - name: E2E and Jest tests + name: E2E Tests # Pin to ubuntu-22.04 for Playwright compatibility # ubuntu-latest (24.04) has library version mismatches with Playwright's WebKit dependencies runs-on: ubuntu-22.04 @@ -131,5 +128,5 @@ jobs: - name: Install WordPress with wp-env run: npm run wp-env start - - name: Run tests - run: npm run test + - name: Run E2E tests + run: npm run test-e2e diff --git a/.github/workflows/js-tests.yml b/.github/workflows/js-tests.yml new file mode 100644 index 00000000..f185470f --- /dev/null +++ b/.github/workflows/js-tests.yml @@ -0,0 +1,60 @@ +name: JS Tests + +on: + push: + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'jest.config.js' + - '.github/workflows/js-tests.yml' + pull_request: + branches: [develop, main] + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'jest.config.js' + - '.github/workflows/js-tests.yml' + workflow_dispatch: + +# Disable all permissions by default; grant minimal permissions per job +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + name: Lint and Jest Tests + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Set up NodeJS 20 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: '20' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run Lint JS + run: npm run lint-js + + - name: Run Jest tests + run: npm run test-jest From 662f87980f6b494c39e120385d73806dc7f12461 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Fri, 9 Jan 2026 13:47:26 +0000 Subject: [PATCH 05/10] fix: prevent Edit Flow nonce checks from killing unrelated forms The notifications module was incorrectly checking the generic `_wpnonce` field against Edit Flow's expected action when posts were saved. This caused contact forms and other plugins that triggered post status transitions to fail, as Edit Flow would call `wp_die()` when their unrelated nonces didn't verify against Edit Flow's action. The fix ensures Edit Flow only processes its own form submissions by checking for the presence of `ef-save_followers` before performing any nonce verification. When Edit Flow's form is submitted, it now verifies against its own dedicated `ef_notifications_nonce` field with the `save_user_usergroups` action, rather than checking the generic `_wpnonce` that other forms might use. Additionally, the AJAX handler `handle_user_post_subscription()` had a security vulnerability where requests without any nonce would pass through due to faulty logic (`!empty && !verify` instead of `empty || !verify`). This has been corrected to properly require a valid nonce. The changes also improve error handling by returning early instead of calling `wp_die()` in the `save_post_subscriptions()` hook. This prevents Edit Flow from terminating requests during the `transition_post_status` action, which fires for all post changes regardless of context. Fixes #882 Co-Authored-By: Claude Opus 4.5 --- modules/notifications/notifications.php | 36 ++- tests/Integration/NotificationsAjaxTest.php | 105 +++++++ .../NotificationsClassicEditorTest.php | 292 ++++++++++++++---- 3 files changed, 357 insertions(+), 76 deletions(-) diff --git a/modules/notifications/notifications.php b/modules/notifications/notifications.php index f27d11d6..3c3acfe0 100644 --- a/modules/notifications/notifications.php +++ b/modules/notifications/notifications.php @@ -554,8 +554,9 @@ function ( $user_id ) use ( $post_id ) { * @since 0.8 */ public function handle_user_post_subscription() { + // Require a valid nonce for this AJAX request. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). - if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) { + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) { $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); } @@ -599,21 +600,30 @@ public function save_post_subscriptions( $new_status, $old_status, $post ) { return; } + // Only process if Edit Flow's followers form was submitted. + if ( ! isset( $_POST['ef-save_followers'] ) ) { + return; + } + + // Check capability. + if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) { + return; + } + + // Verify Edit Flow's own nonce. Return early if missing or invalid - don't die, + // as this hook fires on all post transitions including non-admin contexts. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). - if ( ! empty( $_POST['_wpnonce'] ) && ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post->ID ) ) { - $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + if ( ! isset( $_POST['ef_notifications_nonce'] ) || ! wp_verify_nonce( $_POST['ef_notifications_nonce'], 'save_user_usergroups' ) ) { + return; } - // Only if has edit_post_subscriptions cap. - if ( isset( $_POST['ef-save_followers'] ) && current_user_can( $this->edit_post_subscriptions_cap ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. - $users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : []; - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. - $usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : []; - $this->save_post_following_users( $post, $users ); - if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) { - $this->save_post_following_usergroups( $post, $usergroups ); - } + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. + $users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : []; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. + $usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : []; + $this->save_post_following_users( $post, $users ); + if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) { + $this->save_post_following_usergroups( $post, $usergroups ); } } diff --git a/tests/Integration/NotificationsAjaxTest.php b/tests/Integration/NotificationsAjaxTest.php index bb36d24b..121f036f 100644 --- a/tests/Integration/NotificationsAjaxTest.php +++ b/tests/Integration/NotificationsAjaxTest.php @@ -270,4 +270,109 @@ public function test_ajax_save_persists_usergroup_subscriptions(): void { $edit_flow->user_groups->delete_usergroup( $group1->term_id ); $edit_flow->user_groups->delete_usergroup( $group2->term_id ); } + + /** + * Test: handle_user_post_subscription requires a nonce. + * + * This tests the fix for the nonce check logic. Previously the check was: + * `if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce(...) )` + * which allowed requests without any nonce to pass through. + * + * The fix changes it to: + * `if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce(...) )` + * which requires a valid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_requires_nonce(): void { + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create(); + + // Make request WITHOUT a nonce - this should fail now. + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + // Intentionally NOT setting $_GET['_wpnonce'] + + // print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException. + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify error response. + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is missing' ); + } + + /** + * Test: handle_user_post_subscription fails with invalid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_fails_with_invalid_nonce(): void { + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create(); + + $_GET['_wpnonce'] = 'invalid_nonce'; + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + + // print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException. + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify error response. + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is invalid' ); + } + + /** + * Test: handle_user_post_subscription succeeds with valid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_succeeds_with_valid_nonce(): void { + global $edit_flow; + + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create( + array( + 'post_author' => self::$admin_user_id, + 'post_status' => 'draft', + ) + ); + + $_GET['_wpnonce'] = wp_create_nonce( 'ef_notifications_user_post_subscription' ); + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify we got a success response. + $this->assertNotEmpty( $this->_last_response, 'AJAX should return a response' ); + + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'success', $response['status'], 'Response should indicate success' ); + + // Verify the user is now following the post. + $following_users = $edit_flow->notifications->get_following_users( $post_id, 'id' ); + $this->assertContains( self::$admin_user_id, $following_users, 'User should be following the post' ); + } } diff --git a/tests/Integration/NotificationsClassicEditorTest.php b/tests/Integration/NotificationsClassicEditorTest.php index d25e403c..f63ec1ca 100644 --- a/tests/Integration/NotificationsClassicEditorTest.php +++ b/tests/Integration/NotificationsClassicEditorTest.php @@ -1,8 +1,9 @@ notifications->save_post_subscriptions( 'draft', 'draft', $post ); } catch ( \Exception $e ) { $exception_thrown = true; } - // The function should NOT call wp_die when given a valid Classic Editor nonce. - // If this assertion fails, it means the nonce check is incorrectly rejecting - // valid Classic Editor nonces (the bug reported in 0.10.0). $this->assertFalse( $wp_die_called, - 'save_post_subscriptions should accept valid Classic Editor nonce (update-post_{$post_id}), but it called wp_die instead. This indicates the nonce action string is incorrect.' + 'save_post_subscriptions should not call wp_die when given a valid Edit Flow nonce.' ); $this->assertFalse( $exception_thrown, - 'save_post_subscriptions threw an exception due to wp_die being called with a valid nonce.' + 'save_post_subscriptions should not throw an exception with a valid Edit Flow nonce.' ); } /** - * Test that saving a post via wp_update_post does not fail due to revision nonce mismatch. + * Test that save_post_subscriptions ignores requests without Edit Flow form data. * - * When WordPress saves a post, it creates a revision which triggers transition_post_status - * with a different post ID (the revision ID). The nonce was created for the original post, - * so if save_post_subscriptions doesn't skip revisions, the nonce check will fail. + * When ef-save_followers is not set, the function should return early + * without processing or dying. * - * @ticket https://wordpress.org/support/topic/upgrading-to-0-10-0-breaks-funtionality-for-editor-role/ + * @covers EF_Notifications::save_post_subscriptions */ - public function test_save_post_with_revision_does_not_fail_nonce_check() { - // Create a post. + public function test_save_post_subscriptions_ignores_non_edit_flow_requests() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); + + // Simulate a request without Edit Flow form data (e.g., REST API, other forms). + $_POST = array(); + + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + $exception_thrown = false; + try { + $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should return early without dying when ef-save_followers is not set.' + ); + $this->assertFalse( + $exception_thrown, + 'save_post_subscriptions should not throw when ef-save_followers is not set.' + ); + } + + /** + * Test that save_post_subscriptions doesn't die when another form's nonce is present. + * + * This is the core fix for #882: when a contact form (or any other form) triggers + * a post status transition with its own _wpnonce field, Edit Flow should not + * kill the request by calling wp_die(). + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_does_not_die_with_unrelated_nonce() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); + + // Simulate a contact form submission that happens to trigger post status change. + // The form has its own _wpnonce for a different action (e.g., speaker_submission). + $_POST['_wpnonce'] = wp_create_nonce( 'speaker_submission' ); + // Note: ef-save_followers is NOT set because this is not an Edit Flow form. + + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + $exception_thrown = false; + try { + $edit_flow->notifications->save_post_subscriptions( 'draft', 'publish', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should NOT die when an unrelated form triggers post transition. ' . + 'This was the bug in #882 where contact form submissions failed because Edit Flow ' . + 'was checking the generic _wpnonce field.' + ); + $this->assertFalse( + $exception_thrown, + 'save_post_subscriptions should not throw when an unrelated form triggers post transition.' + ); + } + + /** + * Test that save_post_subscriptions returns early with invalid Edit Flow nonce. + * + * When Edit Flow's form is submitted but the nonce is invalid, the function + * should return early without processing (and importantly, without dying). + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_returns_with_invalid_nonce() { + global $edit_flow; + $post_id = self::factory()->post->create( array( 'post_author' => self::$editor_user_id, @@ -139,12 +243,13 @@ public function test_save_post_with_revision_does_not_fail_nonce_check() { ) ); - // Simulate Classic Editor POST request with a valid nonce for the original post. - $_POST['_wpnonce'] = wp_create_nonce( 'update-post_' . $post_id ); - $_POST['ef-save_followers'] = '1'; - $_POST['ef-selected-users'] = array( self::$editor_user_id ); + $post = get_post( $post_id ); + + // Simulate Edit Flow form submission with an invalid nonce. + $_POST['ef_notifications_nonce'] = 'invalid_nonce_value'; + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); - // Track if wp_die was called. $wp_die_called = false; add_filter( @@ -157,9 +262,60 @@ function () use ( &$wp_die_called ) { } ); - // Update the post which triggers the full save lifecycle including revision creation. - // This will fire transition_post_status for both the post AND the revision. - // The revision has a different ID, so without the revision skip fix, nonce check fails. + $exception_thrown = false; + try { + $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should return early, not die, when nonce is invalid. ' . + 'Dying in a transition_post_status hook kills unrelated functionality.' + ); + $this->assertFalse( + $exception_thrown, + 'save_post_subscriptions should not throw when nonce is invalid.' + ); + } + + /** + * Test that save_post_subscriptions skips revisions. + * + * Revisions have different post IDs which would cause nonce verification to fail + * if not skipped. This test ensures revisions are properly handled. + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_skips_revisions() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + // Simulate Edit Flow form submission. + $_POST['ef_notifications_nonce'] = wp_create_nonce( 'save_user_usergroups' ); + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + // Update the post which triggers revision creation and transition_post_status hooks. $exception_thrown = false; try { wp_update_post( @@ -174,37 +330,47 @@ function () use ( &$wp_die_called ) { $this->assertFalse( $wp_die_called, - 'wp_update_post should not trigger wp_die - save_post_subscriptions must skip revisions to avoid nonce mismatch' + 'save_post_subscriptions should skip revisions to avoid nonce mismatch issues.' ); $this->assertFalse( $exception_thrown, - 'wp_update_post threw an exception due to wp_die being called during revision save' + 'wp_update_post should not throw due to revision handling.' ); } /** - * Test that the nonce verification uses the correct action string. + * Test that subscriptions are saved when valid Edit Flow nonce is provided. * - * WordPress Classic Editor uses 'update-post_{$post_id}' for the edit form nonce. - * This test verifies that a nonce created with this action is considered valid. + * @covers EF_Notifications::save_post_subscriptions */ - public function test_classic_editor_nonce_action_is_update_post() { - $post_id = self::factory()->post->create(); - - // Create a nonce using the Classic Editor action. - $nonce = wp_create_nonce( 'update-post_' . $post_id ); + public function test_save_post_subscriptions_saves_data_with_valid_nonce() { + global $edit_flow; - // Verify the nonce is valid for the correct action. - $this->assertNotFalse( - wp_verify_nonce( $nonce, 'update-post_' . $post_id ), - 'Nonce should be valid for update-post_{$post_id} action' + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) ); - // The buggy code uses 'editpost' which is NOT a valid WordPress nonce action. - // This should fail to verify. - $this->assertFalse( - wp_verify_nonce( $nonce, 'editpost' ), - 'Nonce created for update-post_{$post_id} should NOT verify against editpost action' + $post = get_post( $post_id ); + + // Simulate Edit Flow form submission with valid nonce. + $_POST['ef_notifications_nonce'] = wp_create_nonce( 'save_user_usergroups' ); + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + // Call the function. + $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); + + // Verify the subscription was saved. + // Note: get_following_users with 'id' returns IDs as integers. + $following_users = $edit_flow->notifications->get_following_users( $post_id, 'id' ); + + $this->assertContains( + self::$editor_user_id, + $following_users, + 'User should be saved as a follower when valid Edit Flow nonce is provided.' ); } } From ea4293834a915c82efd755a71c9a331b6415303f Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 11 Jan 2026 16:37:27 +0000 Subject: [PATCH 06/10] fix: add missing imports for Extended Post Status block editor panel The Extended Post Status dropdown was not appearing in the block editor because SelectControl and PluginPostStatusInfo were used but never imported, causing the registerPlugin call to fail silently. Also refactored webpack.config.js to use separate configurations for each entry point: custom-status-block now uses default wp-scripts externalization since it runs in the block editor where @wordpress packages are available as globals, while calendar-react continues to bundle them for use outside the editor context. Co-Authored-By: Claude Opus 4.5 --- .../custom-status/lib/custom-status-block.js | 2 ++ webpack.config.js | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/modules/custom-status/lib/custom-status-block.js b/modules/custom-status/lib/custom-status-block.js index e5bd7383..8dd50207 100644 --- a/modules/custom-status/lib/custom-status-block.js +++ b/modules/custom-status/lib/custom-status-block.js @@ -1,9 +1,11 @@ import './editor.scss'; import './style.scss'; +import { SelectControl } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { subscribe, dispatch, select } from '@wordpress/data'; import { withSelect, withDispatch } from '@wordpress/data'; +import { PluginPostStatusInfo } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { registerPlugin } from '@wordpress/plugins'; diff --git a/webpack.config.js b/webpack.config.js index c13628de..ca030abc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,4 @@ +const path = require( 'path' ); const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); @@ -20,7 +21,7 @@ const shouldBundleRequest = ( request ) => { // Replace the default DependencyExtractionWebpackPlugin with a configured one // Return false to explicitly prevent externalization of bundled packages -const plugins = defaultConfig.plugins.map( ( plugin ) => { +const calendarPlugins = defaultConfig.plugins.map( ( plugin ) => { if ( plugin.constructor.name === 'DependencyExtractionWebpackPlugin' ) { return new DependencyExtractionWebpackPlugin( { requestToExternal( request ) { @@ -40,11 +41,33 @@ const plugins = defaultConfig.plugins.map( ( plugin ) => { return plugin; } ); -module.exports = { +// Custom status block runs in the block editor where @wordpress packages are +// available as globals, so use default externalization behavior. +const customStatusBlockConfig = { ...defaultConfig, entry: { 'custom-status-block': './modules/custom-status/lib/custom-status-block.js', + }, + output: { + ...defaultConfig.output, + path: path.resolve( __dirname, 'build' ), + clean: false, + }, +}; + +// Calendar React needs @wordpress packages bundled since it runs outside +// the block editor context. +const calendarReactConfig = { + ...defaultConfig, + entry: { 'calendar-react': './modules/calendar/lib/react/calendar.react.js', }, - plugins, + output: { + ...defaultConfig.output, + path: path.resolve( __dirname, 'build' ), + clean: false, + }, + plugins: calendarPlugins, }; + +module.exports = [ customStatusBlockConfig, calendarReactConfig ]; From 287424874ef5ecd074e9c74d2593e56e22b009fc Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 11 Jan 2026 15:20:45 +0000 Subject: [PATCH 07/10] feat: add WordPress Playground blueprint for live preview Enable the WordPress.org "Live Preview" feature by adding a Playground blueprint configuration. This allows potential users to explore Edit Flow's features interactively before installation. The blueprint demonstrates the plugin's editorial workflow capabilities by creating a realistic scenario with multiple users (editor and writer), sample posts across different custom statuses (pitch, assigned, in-progress, pending, draft, scheduled), and editorial comments showing team collaboration. Users land directly on the Calendar view to immediately showcase the editorial calendar feature. Inspired by Co-Authors Plus PR #1184. Co-Authored-By: Claude Opus 4.5 --- .wordpress-org/blueprints/blueprint.json | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .wordpress-org/blueprints/blueprint.json diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json new file mode 100644 index 00000000..a903a3b9 --- /dev/null +++ b/.wordpress-org/blueprints/blueprint.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "landingPage": "/wp-admin/index.php?page=calendar", + "preferredVersions": { + "php": "8.2", + "wp": "latest" + }, + "phpExtensionBundles": [ + "kitchen-sink" + ], + "steps": [ + { + "step": "installPlugin", + "pluginData": { + "resource": "wordpress.org/plugins", + "slug": "edit-flow" + }, + "options": { + "activate": true + } + }, + { + "step": "runPHP", + "code": "custom_status ) ) { EditFlow()->custom_status->install(); } if ( isset( EditFlow()->calendar ) ) { EditFlow()->calendar->install(); } } $role = get_role( 'administrator' ); if ( $role ) { $role->add_cap( 'ef_view_calendar' ); } ?>" + }, + { + "step": "runPHP", + "code": "set_role( 'editor' ); update_user_meta( $user_id, 'first_name', 'Sarah' ); update_user_meta( $user_id, 'last_name', 'Editor' ); wp_update_user( array( 'ID' => $user_id, 'display_name' => 'Sarah Editor' ) ); } ?>" + }, + { + "step": "runPHP", + "code": "set_role( 'author' ); update_user_meta( $user_id, 'first_name', 'James' ); update_user_meta( $user_id, 'last_name', 'Writer' ); wp_update_user( array( 'ID' => $user_id, 'display_name' => 'James Writer' ) ); } ?>" + }, + { + "step": "runPHP", + "code": " 'New Feature: AI-Powered Content Suggestions', 'post_content' => 'This article explores how AI can help content creators brainstorm and refine their ideas.', 'post_status' => 'pitch', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+3 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Guide to Remote Team Collaboration', 'post_content' => 'Remote work has transformed how editorial teams collaborate.', 'post_status' => 'assigned', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+5 days' ) ), ) ); $progress_post = wp_insert_post( array( 'post_title' => '10 Tips for Better Editorial Workflows', 'post_content' => 'Streamlining your editorial workflow can save hours each week.', 'post_status' => 'in-progress', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), ) ); update_option( 'ef_demo_progress_post_id', $progress_post ); wp_insert_post( array( 'post_title' => 'The Future of Digital Publishing', 'post_content' => 'Digital publishing continues to evolve rapidly.', 'post_status' => 'pending', 'post_author' => $editor->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+2 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Interview: Leading Through Change', 'post_content' => 'In this exclusive interview, we speak with industry leaders.', 'post_status' => 'draft', 'post_author' => 1, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+7 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Weekly Roundup: Content Strategy Insights', 'post_content' => 'This week in content strategy: new research on reader engagement.', 'post_status' => 'future', 'post_author' => $editor->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+4 days' ) ), ) ); ?>" + }, + { + "step": "runPHP", + "code": " $post_id, 'comment_content' => 'Great start! Could you expand on tip #3 about visual calendars?', 'comment_type' => 'editorial-comment', 'user_id' => $editor->ID, 'comment_author' => $editor->display_name, 'comment_author_email' => $editor->user_email, 'comment_approved' => 'editorial-comment', ) ); wp_insert_comment( array( 'comment_post_ID' => $post_id, 'comment_content' => 'Thanks Sarah! I will add a calendar screenshot and expand that section.', 'comment_type' => 'editorial-comment', 'user_id' => $writer->ID, 'comment_author' => $writer->display_name, 'comment_author_email' => $writer->user_email, 'comment_approved' => 'editorial-comment', ) ); delete_option( 'ef_demo_progress_post_id' ); } ?>" + }, + { + "step": "login", + "username": "admin", + "password": "password" + } + ] +} From d1f23f23a02c6452e1cf70245682942a0ec83895 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 12 Jan 2026 15:46:21 +0000 Subject: [PATCH 08/10] Version 0.10.3 changelog --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f06ace..25023ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.3] - 2026-01-12 + +### Added + +* feat: add WordPress Playground blueprint for live preview by @GaryJones in [#885](https://github.com/Automattic/Edit-Flow/pull/885) + +### Fixed + +* fix: prevent Edit Flow nonce checks from killing unrelated forms by @GaryJones in [#883](https://github.com/Automattic/Edit-Flow/pull/883) +* fix: add missing imports for Extended Post Status block editor panel by @GaryJones in [#884](https://github.com/Automattic/Edit-Flow/pull/884) + +### Documentation + +* docs: move Development section from README to CONTRIBUTING by @GaryJones in [#880](https://github.com/Automattic/Edit-Flow/pull/880) + +### Maintenance + +* ci: optimise CI workflows with path filters and split tests by @GaryJones in [#881](https://github.com/Automattic/Edit-Flow/pull/881) +* test: add integration test for revision nonce handling by @GaryJones in [#879](https://github.com/Automattic/Edit-Flow/pull/879) + ## [0.10.2] - 2026-01-07 ### Fixed @@ -395,6 +415,7 @@ This is a major update with significant bug fixes, new features, and modernised * Ability to assign custom statuses to posts. +[0.10.3]: https://github.com/Automattic/Edit-Flow/compare/0.10.2...0.10.3 [0.10.2]: https://github.com/Automattic/Edit-Flow/compare/0.10.1...0.10.2 [0.10.1]: https://github.com/Automattic/Edit-Flow/compare/0.10.0...0.10.1 [0.10.0]: https://github.com/Automattic/Edit-Flow/compare/0.9.9...0.10.0 From a4693ccdc4f27ed7d944b7b337d3c70a5be993a6 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 12 Jan 2026 15:46:30 +0000 Subject: [PATCH 09/10] Version 0.10.3 i18n --- languages/edit-flow.pot | 110 +++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/languages/edit-flow.pot b/languages/edit-flow.pot index 2f0ee2d0..9f4b0063 100644 --- a/languages/edit-flow.pot +++ b/languages/edit-flow.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: Edit Flow 0.10.2\n" +"Project-Id-Version: Edit Flow 0.10.3\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/edit-flow\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-01-07T16:12:56+00:00\n" +"POT-Creation-Date: 2026-01-12T13:39:35+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: edit-flow\n" @@ -347,8 +347,8 @@ msgstr "" #: modules/dashboard/dashboard.php:276 #: modules/dashboard/dashboard.php:296 #: modules/dashboard/dashboard.php:324 -#: modules/notifications/notifications.php:1472 -#: modules/notifications/notifications.php:1491 +#: modules/notifications/notifications.php:1482 +#: modules/notifications/notifications.php:1501 msgid "Disabled" msgstr "" @@ -357,8 +357,8 @@ msgstr "" #: modules/dashboard/dashboard.php:277 #: modules/dashboard/dashboard.php:297 #: modules/dashboard/dashboard.php:325 -#: modules/notifications/notifications.php:1473 -#: modules/notifications/notifications.php:1492 +#: modules/notifications/notifications.php:1483 +#: modules/notifications/notifications.php:1502 msgid "Enabled" msgstr "" @@ -931,7 +931,7 @@ msgid "No users or groups were notified." msgstr "" #: modules/editorial-comments/editorial-comments.php:241 -#: modules/notifications/notifications.php:888 +#: modules/notifications/notifications.php:898 msgid "Notified" msgstr "" @@ -1146,7 +1146,7 @@ msgstr "" #. translators: 1: date, 2: time #: modules/editorial-metadata/editorial-metadata.php:958 -#: modules/notifications/notifications.php:1582 +#: modules/notifications/notifications.php:1592 #, php-format msgid "%1$s at %2$s" msgstr "" @@ -1401,217 +1401,217 @@ msgstr "" msgid "Nonce check failed. Please ensure you can add users or user groups to a post." msgstr "" -#: modules/notifications/notifications.php:705 +#: modules/notifications/notifications.php:715 msgid "WordPress Scheduler" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:736 +#: modules/notifications/notifications.php:746 #, php-format msgid "[%1$s] New %2$s Created: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:738 +#: modules/notifications/notifications.php:748 #, php-format msgid "A new %1$s (#%2$s \"%3$s\") was created by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:741 +#: modules/notifications/notifications.php:751 #, php-format msgid "[%1$s] %2$s Trashed: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:743 +#: modules/notifications/notifications.php:753 #, php-format msgid "%1$s #%2$s \"%3$s\" was moved to the trash by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:746 +#: modules/notifications/notifications.php:756 #, php-format msgid "[%1$s] %2$s Restored (from Trash): \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:748 +#: modules/notifications/notifications.php:758 #, php-format msgid "%1$s #%2$s \"%3$s\" was restored from trash by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:751 +#: modules/notifications/notifications.php:761 #, php-format msgid "[%1$s] %2$s Scheduled: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email 6. scheduled date -#: modules/notifications/notifications.php:753 +#: modules/notifications/notifications.php:763 #, php-format msgid "%1$s #%2$s \"%3$s\" was scheduled by %4$s %5$s. It will be published on %6$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:756 +#: modules/notifications/notifications.php:766 #, php-format msgid "[%1$s] %2$s Published: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:758 +#: modules/notifications/notifications.php:768 #, php-format msgid "%1$s #%2$s \"%3$s\" was published by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:761 +#: modules/notifications/notifications.php:771 #, php-format msgid "[%1$s] %2$s Unpublished: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:763 +#: modules/notifications/notifications.php:773 #, php-format msgid "%1$s #%2$s \"%3$s\" was unpublished by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:766 +#: modules/notifications/notifications.php:776 #, php-format msgid "[%1$s] %2$s Status Changed for \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:768 +#: modules/notifications/notifications.php:778 #, php-format msgid "Status was changed for %1$s #%2$s \"%3$s\" by %4$s %5$s" msgstr "" #. translators: 1: date, 2: time, 3: timezone -#: modules/notifications/notifications.php:772 +#: modules/notifications/notifications.php:782 #, php-format msgid "This action was taken on %1$s at %2$s %3$s" msgstr "" #. translators: 1: old status, 2: new status -#: modules/notifications/notifications.php:777 +#: modules/notifications/notifications.php:787 #, php-format msgid "%1$s => %2$s" msgstr "" #. translators: 1: post type -#: modules/notifications/notifications.php:783 +#: modules/notifications/notifications.php:793 #, php-format msgid "== %s Details ==" msgstr "" #. translators: 1: post title -#: modules/notifications/notifications.php:785 +#: modules/notifications/notifications.php:795 #, php-format msgid "Title: %s" msgstr "" #. translators: 1: author name, 2: author email -#: modules/notifications/notifications.php:788 +#: modules/notifications/notifications.php:798 #, php-format msgid "Author: %1$s (%2$s)" msgstr "" -#: modules/notifications/notifications.php:810 -#: modules/notifications/notifications.php:906 +#: modules/notifications/notifications.php:820 +#: modules/notifications/notifications.php:916 msgid "== Actions ==" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:812 -#: modules/notifications/notifications.php:910 +#: modules/notifications/notifications.php:822 +#: modules/notifications/notifications.php:920 #, php-format msgid "Add editorial comment: %s" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:814 -#: modules/notifications/notifications.php:912 +#: modules/notifications/notifications.php:824 +#: modules/notifications/notifications.php:922 #, php-format msgid "Edit: %s" msgstr "" #. translators: 1: view link -#: modules/notifications/notifications.php:816 -#: modules/notifications/notifications.php:914 +#: modules/notifications/notifications.php:826 +#: modules/notifications/notifications.php:924 #, php-format msgid "View: %s" msgstr "" #. translators: 1: user name, 2: post type, 3: post id, 4: edit link, 5: post title, 6: old status, 7: new status -#: modules/notifications/notifications.php:824 +#: modules/notifications/notifications.php:834 #, php-format msgid "*%1$s* changed the status of *%2$s #%3$s - <%4$s|%5$s>* from *%6$s* to *%7$s*" msgstr "" #. translators: 1: blog name, 2: post title -#: modules/notifications/notifications.php:876 +#: modules/notifications/notifications.php:886 #, php-format msgid "[%1$s] New Editorial Comment: \"%2$s\"" msgstr "" #. translators: 1: post id, 2: post title, 3. post type -#: modules/notifications/notifications.php:879 +#: modules/notifications/notifications.php:889 #, php-format msgid "A new editorial comment was added to %3$s #%1$s \"%2$s\"" msgstr "" #. translators: 1: comment author, 2: author email, 3: date, 4: time -#: modules/notifications/notifications.php:881 +#: modules/notifications/notifications.php:891 #, php-format msgid "%1$s (%2$s) said on %3$s at %4$s:" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:908 +#: modules/notifications/notifications.php:918 #, php-format msgid "Reply: %s" msgstr "" #. translators: 1: post type -#: modules/notifications/notifications.php:917 +#: modules/notifications/notifications.php:927 #, php-format msgid "You can see all editorial comments on this %s here: " msgstr "" #. translators: 1: comment author, 2: post type, 3: post id, 4: edit link, 5: post title, 6: comment content -#: modules/notifications/notifications.php:926 +#: modules/notifications/notifications.php:936 #, php-format msgid "*%1$s* left a comment on *%2$s #%3$s - <%4$s|%5$s>*" msgstr "" #. translators: 1: post title -#: modules/notifications/notifications.php:944 +#: modules/notifications/notifications.php:954 #, php-format msgid "You are receiving this email because you are subscribed to \"%s\"." msgstr "" #. translators: 1: date -#: modules/notifications/notifications.php:948 +#: modules/notifications/notifications.php:958 #, php-format msgid "This email was sent %s." msgstr "" -#: modules/notifications/notifications.php:1449 +#: modules/notifications/notifications.php:1459 msgid "Post types for notifications:" msgstr "" -#: modules/notifications/notifications.php:1450 +#: modules/notifications/notifications.php:1460 msgid "Always notify blog admin" msgstr "" -#: modules/notifications/notifications.php:1451 +#: modules/notifications/notifications.php:1461 msgid "Send to Webhook" msgstr "" -#: modules/notifications/notifications.php:1452 +#: modules/notifications/notifications.php:1462 msgid "Webhook URL" msgstr "" @@ -2120,22 +2120,26 @@ msgstr "" msgid "Number of weeks" msgstr "" -#: modules/custom-status/lib/custom-status-block.js:102 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:104 #: dist/custom-status.build.js:121 msgid "Extended Post Status" msgstr "" -#: modules/custom-status/lib/custom-status-block.js:103 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:105 #: dist/custom-status.build.js:122 msgid "Extended Post Status Disabled." msgstr "" -#: modules/custom-status/lib/custom-status-block.js:119 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:121 #: dist/custom-status.build.js:138 msgid "Note: this will override all status settings above." msgstr "" -#: modules/custom-status/lib/custom-status-block.js:120 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:122 #: dist/custom-status.build.js:139 msgid "To select a custom status, please unpublish the content first." msgstr "" From cddcb5ac77d6e45c9c002e2fc482b364ed553c73 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 12 Jan 2026 15:46:44 +0000 Subject: [PATCH 10/10] Version 0.10.3 --- .claude/CLAUDE.md | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 2 +- edit_flow.php | 4 ++-- package.json | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .claude/CLAUDE.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..77635e31 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,38 @@ +# Edit Flow + +Editorial workflow plugin with custom statuses, editorial comments, and notifications. + +## Plugin Details + +| Property | Value | +|----------|-------| +| **Main file** | `edit_flow.php` | +| **Text domain** | `edit-flow` | +| **Function prefix** | `ef_` | +| **Namespace** | Global (legacy) | +| **Source directory** | `modules/` | +| **Version** | 0.10.3 | + +## Architecture + +- Modular architecture in `modules/` directory +- Each feature is a separate module +- Main class: `edit_flow` (note underscore in filename) + +## Testing + +```bash +composer test:unit # Unit tests +composer test:integration # Integration tests +npm run test-e2e # Playwright E2E tests +``` + +## Notes + +- Tier 1 plugin (well-maintained) +- WordPress.org hosted +- Has E2E tests with Playwright + +## Standards + +Follow the standards documented in `~/code/plugin-standards/`. diff --git a/README.md b/README.md index bae753e2..66b2b7b7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Tags: workflow, editorial, editorial calendar, custom status, newsroom Requires at least: 6.4 Requires PHP: 7.4 Tested up to: 6.9 -Stable tag: 0.10.2 +Stable tag: 0.10.3 Redefining your editorial workflow. diff --git a/edit_flow.php b/edit_flow.php index 8b688c52..45898af5 100644 --- a/edit_flow.php +++ b/edit_flow.php @@ -4,7 +4,7 @@ * Plugin URI: http://editflow.org/ * Description: Remixing the WordPress admin for better editorial workflow options. * Author: Daniel Bachhuber, Scott Bressler, Mohammad Jangda, Automattic, and others - * Version: 0.10.2 + * Version: 0.10.3 * Requires at least: 6.4 * Requires PHP: 7.4 * License: GPLv2 or later @@ -34,7 +34,7 @@ function _ef_print_php_version_admin_notice() { } // Define constants. -define( 'EDIT_FLOW_VERSION', '0.10.0' ); +define( 'EDIT_FLOW_VERSION', '0.10.3' ); define( 'EDIT_FLOW_ROOT', __DIR__ ); define( 'EDIT_FLOW_FILE_PATH', EDIT_FLOW_ROOT . '/' . basename( __FILE__ ) ); define( 'EDIT_FLOW_URL', plugins_url( '/', __FILE__ ) ); diff --git a/package.json b/package.json index 4df48f7d..0b1d01ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edit-flow", - "version": "0.10.0", + "version": "0.10.3", "description": "Edit Flow", "directories": { "test": "tests"