From ebd903e1f2fd7739aecf9d35b8eb38d2c752d020 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Tue, 28 Jul 2026 11:51:23 +0300 Subject: [PATCH 1/7] fix: prevent fatal errors when LinkedIn authorization fails LinkedIn authorization failures could crash to the WordPress critical-error screen instead of surfacing the LinkedIn error: - authorize() called the non-existent Exception::getDescription() in its catch block, turning any caught error into a second fatal - add_account_with_app() ran array_pop() on the result of unserialize() without validating the payload, fataling on PHP 8 when the popup posts back an error payload instead of account data - add_account_li() ignored the add_account_with_app() return value and registered the service regardless - sign-in-btn.vue parsed every popup message as account data Validation failures now log the LinkedIn error to the Revive Social log and answer the REST call with a code 400 response. Adds e2e coverage for the malformed-payload paths (red on the old code) plus a happy-path guard, and drops the stale PHPStan baseline entry for the fixed getDescription() call. Co-Authored-By: Claude Fable 5 --- includes/admin/class-rop-rest-api.php | 10 +- .../services/class-rop-linkedin-service.php | 18 +++- phpstan-baseline.neon | 6 -- .../dashboard/linkedin-error-handling.spec.js | 96 +++++++++++++++++++ vue/src/vue-elements/sign-in-btn.vue | 16 +++- 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 tests/e2e/specs/dashboard/linkedin-error-handling.spec.js diff --git a/includes/admin/class-rop-rest-api.php b/includes/admin/class-rop-rest-api.php index c9c7fb8cc..a77dafe31 100644 --- a/includes/admin/class-rop-rest-api.php +++ b/includes/admin/class-rop-rest-api.php @@ -1194,7 +1194,15 @@ private function add_account_li( $data ) { $model = new Rop_Services_Model(); $db = new Rop_Db_Upgrade(); - $linkedin_service->add_account_with_app( $data ); + $added = $linkedin_service->add_account_with_app( $data ); + + if ( ! $added ) { + $this->response->set_code( '400' ) + ->set_message( 'Could not add the LinkedIn account, the authorization data was invalid. Check the Revive Social log for details.' ) + ->set_data( array() ); + + return $this->response->to_array(); + } $services[ $linkedin_service->get_service_id() ] = $linkedin_service->get_service(); $active_accounts = array_merge( $active_accounts, $linkedin_service->get_service_active_accounts() ); diff --git a/includes/admin/services/class-rop-linkedin-service.php b/includes/admin/services/class-rop-linkedin-service.php index 15b4feacf..1b06c74d9 100644 --- a/includes/admin/services/class-rop-linkedin-service.php +++ b/includes/admin/services/class-rop-linkedin-service.php @@ -135,10 +135,11 @@ public function authorize() { } } catch ( Exception $e ) { - $message = 'Linkedin Error: Code[ ' . $e->getCode() . ' ] ' . $e->getDescription(); + $description = method_exists( $e, 'getDescription' ) ? $e->getDescription() : $e->getMessage(); + $message = 'Linkedin Error: Code[ ' . $e->getCode() . ' ] ' . $description; $this->logger->alert_error( $message ); $this->rop_get_error_docs( $message ); - $referrer = $_SERVER['HTTP_REFERER']; + $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : ''; // If the user is trying to authenticate. if ( ! empty( substr_count( $referrer, 'linkedin.com' ) ) ) { exit( wp_redirect( $this->get_legacy_url() ) ); @@ -838,15 +839,26 @@ function ( $matches ) { * @access public */ public function add_account_with_app( $accounts_data ) { - if ( ! $this->is_set_not_empty( $accounts_data, array( 'id' ) ) ) { + if ( ! $this->is_set_not_empty( $accounts_data, array( 'id', 'pages' ) ) ) { return false; } $the_id = unserialize( base64_decode( $accounts_data['id'] ) ); $accounts_array = unserialize( base64_decode( $accounts_data['pages'] ) ); + if ( empty( $the_id ) || ! is_array( $accounts_array ) ) { + $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); + return false; + } + // last array item contains notify date $notify_user_at = array_pop( $accounts_array ); + + if ( empty( $accounts_array ) || ! is_array( $notify_user_at ) || ! isset( $notify_user_at['notify_user_at'] ) ) { + $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); + return false; + } + // save timestamp for when to notify user to refresh their linkedin token // set notified count to 0 $notify_data = array( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e09ffa961..5e5aa0e61 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3930,12 +3930,6 @@ parameters: count: 4 path: includes/admin/services/class-rop-gmb-service.php - - - message: '#^Call to an undefined method Exception\:\:getDescription\(\)\.$#' - identifier: method.notFound - count: 1 - path: includes/admin/services/class-rop-linkedin-service.php - - message: '#^Call to function is_object\(\) with LinkedIn\\Client will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js new file mode 100644 index 000000000..b29d80edf --- /dev/null +++ b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js @@ -0,0 +1,96 @@ +/** + * WordPress dependencies + */ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +// base64( serialize( 'urn:li:person:E2ETEST' ) ) +const VALID_ID = 'czoyMToidXJuOmxpOnBlcnNvbjpFMkVURVNUIjs='; +// base64( serialize( [ account array, [ 'notify_user_at' => 4102444800 ] ] ) ) +const VALID_PAGES = 'YToyOntpOjA7YTo2OntzOjI6ImlkIjtzOjIxOiJ1cm46bGk6cGVyc29uOkUyRVRFU1QiO3M6MzoiaW1nIjtzOjA6IiI7czo3OiJhY2NvdW50IjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEwOiJpc19jb21wYW55IjtiOjA7czo0OiJ1c2VyIjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO3M6MTQ6ImUyZS10ZXN0LXRva2VuIjt9aToxO2E6MTp7czoxNDoibm90aWZ5X3VzZXJfYXQiO2k6NDEwMjQ0NDgwMDt9fQ=='; + +/** + * Call a plugin API endpoint from the dashboard page. + * + * @param {import('@playwright/test').Page} page The page object. + * @param {string} req The API method to call. + * @param {Object} body The request payload. + */ +async function callRopApi( page, req, body ) { + return await page.evaluate( async ( { req, body } ) => { + const response = await fetch( `${window.ropApiSettings.root}&req=${req}`, { + method: 'POST', + body: JSON.stringify( body ), + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': window.ropApiSettings.nonce, + }, + } ); + + const text = await response.text(); + let json = null; + try { + json = JSON.parse( text ); + } catch ( e ) { + // Non-JSON response, e.g. the plugin's ROP_DEBUG output. + } + + return { status: response.status, body: json, text }; + }, { req, body } ); +} + +test.describe( 'LinkedIn error handling (issue #1098)', () => { + + test.beforeEach( async ( { page, admin } ) => { + await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); + await page.waitForSelector( '.tab-view[type="accounts"]' ); + } ); + + test( 'error payload without pages is rejected without a fatal error', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { id: VALID_ID } ); + + // Before the fix this fataled (array_pop on bool) and surfaced as a + // WordPress critical-error response. After the fix the payload is + // rejected by validation: with ROP_DEBUG on the plugin answers with + // its debug text, in production with a JSON code 400 response. + expect( response.text ).not.toContain( 'critical error' ); + if ( response.body ) { + expect( response.body.code ).toBe( '400' ); + } else { + expect( response.text ).toContain( 'Value not set' ); + } + + // The dashboard must survive the failed attempt. + await page.reload(); + await page.waitForSelector( '.tab-view[type="accounts"]' ); + await expect( page.getByRole( 'button', { name: 'LinkedIn' } ) ).toBeVisible(); + } ); + + test( 'garbled pages payload is rejected without a fatal error', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: btoa( 'linkedin-error-string-not-account-data' ), + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + + // No LinkedIn service must have been registered from the bad payload. + const services = await callRopApi( page, 'get_authenticated_services', {} ); + const serviceNames = Object.values( services.body.data || {} ).map( ( s ) => s.service ); + expect( serviceNames ).not.toContain( 'linkedin' ); + } ); + + test( 'valid payload still adds the account', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: VALID_PAGES, + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '200' ); + + // Clean up so other specs start from a pristine accounts state. + const reset = await callRopApi( page, 'reset_accounts', {} ); + expect( reset.body.code ).toBe( '200' ); + } ); +} ); diff --git a/vue/src/vue-elements/sign-in-btn.vue b/vue/src/vue-elements/sign-in-btn.vue index 0f5347490..8c4d8a828 100644 --- a/vue/src/vue-elements/sign-in-btn.vue +++ b/vue/src/vue-elements/sign-in-btn.vue @@ -892,7 +892,21 @@ export default { return; } - const accountData = JSON.parse(event.data); + let accountData; + try { + accountData = JSON.parse(event.data); + } catch (e) { + this.is_loading = false; + Vue.$log.error('Received a malformed message from the authorization window', e); + return; + } + + if ( ! accountData || 'object' !== typeof accountData || accountData.error ) { + this.is_loading = false; + window.removeEventListener("message", this.getChildWindowMessage ); + Vue.$log.error('Authorization failed', accountData); + return; + } if ('Twitter' === this.modal.serviceName) { this.addAccountTW( accountData ); From eaadc9c0ddc375b6f955cb32ba34940df26ce754 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Tue, 28 Jul 2026 12:04:38 +0300 Subject: [PATCH 2/7] test: expand LinkedIn e2e regression coverage Adds regression tests for the remaining validation branches: empty payload, pages without a notify entry, notify-only pages with no accounts (asserting no service gets registered), the LinkedIn error landing in the plugin log, and the happy path now verifies the account is actually registered and exposed. Co-Authored-By: Claude Fable 5 --- .../dashboard/linkedin-error-handling.spec.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js index b29d80edf..918e65b31 100644 --- a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js +++ b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js @@ -7,6 +7,10 @@ import { test, expect } from '@wordpress/e2e-test-utils-playwright'; const VALID_ID = 'czoyMToidXJuOmxpOnBlcnNvbjpFMkVURVNUIjs='; // base64( serialize( [ account array, [ 'notify_user_at' => 4102444800 ] ] ) ) const VALID_PAGES = 'YToyOntpOjA7YTo2OntzOjI6ImlkIjtzOjIxOiJ1cm46bGk6cGVyc29uOkUyRVRFU1QiO3M6MzoiaW1nIjtzOjA6IiI7czo3OiJhY2NvdW50IjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEwOiJpc19jb21wYW55IjtiOjA7czo0OiJ1c2VyIjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO3M6MTQ6ImUyZS10ZXN0LXRva2VuIjt9aToxO2E6MTp7czoxNDoibm90aWZ5X3VzZXJfYXQiO2k6NDEwMjQ0NDgwMDt9fQ=='; +// Same as VALID_PAGES but the last entry is not the notify date. +const PAGES_WITHOUT_NOTIFY = 'YToyOntpOjA7YTo2OntzOjI6ImlkIjtzOjIxOiJ1cm46bGk6cGVyc29uOkUyRVRFU1QiO3M6MzoiaW1nIjtzOjA6IiI7czo3OiJhY2NvdW50IjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEwOiJpc19jb21wYW55IjtiOjA7czo0OiJ1c2VyIjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO3M6MTQ6ImUyZS10ZXN0LXRva2VuIjt9aToxO2E6MTp7czoxMDoidW5leHBlY3RlZCI7aToxO319'; +// base64( serialize( [ [ 'notify_user_at' => ... ] ] ) ) — notify entry only, no accounts. +const PAGES_ONLY_NOTIFY = 'YToxOntpOjA7YToxOntzOjE0OiJub3RpZnlfdXNlcl9hdCI7aTo0MTAyNDQ0ODAwO319'; /** * Call a plugin API endpoint from the dashboard page. @@ -80,6 +84,51 @@ test.describe( 'LinkedIn error handling (issue #1098)', () => { expect( serviceNames ).not.toContain( 'linkedin' ); } ); + test( 'empty payload is rejected without a fatal error', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', {} ); + + expect( response.text ).not.toContain( 'critical error' ); + if ( response.body ) { + expect( response.body.code ).toBe( '400' ); + } else { + expect( response.text ).toContain( 'Value not set' ); + } + } ); + + test( 'pages without a notify entry are rejected', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: PAGES_WITHOUT_NOTIFY, + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + } ); + + test( 'pages with only a notify entry and no accounts are rejected', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: PAGES_ONLY_NOTIFY, + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + + const services = await callRopApi( page, 'get_authenticated_services', {} ); + const serviceNames = Object.values( services.body.data || {} ).map( ( s ) => s.service ); + expect( serviceNames ).not.toContain( 'linkedin' ); + } ); + + test( 'rejected payload leaves the LinkedIn error in the plugin log', async ( { page } ) => { + await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: btoa( 'linkedin-error-string-not-account-data' ), + } ); + + const log = await callRopApi( page, 'get_log', {} ); + expect( JSON.stringify( log.body.data ) ).toContain( 'Linkedin Error' ); + } ); + test( 'valid payload still adds the account', async ( { page } ) => { const response = await callRopApi( page, 'add_account_li', { id: VALID_ID, @@ -89,6 +138,14 @@ test.describe( 'LinkedIn error handling (issue #1098)', () => { expect( response.status ).toBe( 200 ); expect( response.body.code ).toBe( '200' ); + // The service must actually be registered, with the account exposed. + const services = await callRopApi( page, 'get_authenticated_services', {} ); + const linkedin = Object.values( services.body.data || {} ).find( + ( s ) => s.service === 'linkedin' + ); + expect( linkedin ).toBeTruthy(); + expect( JSON.stringify( linkedin.available_accounts ) ).toContain( 'E2E Test User' ); + // Clean up so other specs start from a pristine accounts state. const reset = await callRopApi( page, 'reset_accounts', {} ); expect( reset.body.code ).toBe( '200' ); From f452438a6e29884b7ad25ae293e92a5d99b33191 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Tue, 28 Jul 2026 12:22:03 +0300 Subject: [PATCH 3/7] fix: drop PHPStan baseline entry resolved by the payload validation The add_account_with_app() validation guarantees the accounts loop always runs, so PHPStan no longer reports 'Variable $account might not be defined' and the baseline ignore became unmatched. Co-Authored-By: Claude Fable 5 --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5e5aa0e61..a3659edd2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4146,12 +4146,6 @@ parameters: count: 1 path: includes/admin/services/class-rop-linkedin-service.php - - - message: '#^Variable \$account might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: includes/admin/services/class-rop-linkedin-service.php - - message: '#^Variable \$data in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable From 50f9443ee4a9f42e1ece174cba92c5c6902001c9 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Wed, 29 Jul 2026 10:24:37 +0300 Subject: [PATCH 4/7] fix: harden LinkedIn payload validation per review feedback Addresses the Copilot review on #1100: - require the decoded account id to be a non-empty string so it cannot reach str_replace()/array-key usage as an array or object - validate that the notify entry timestamp is numeric before persisting the refresh-token notice - validate every remaining pages entry is a complete account array before reading its fields, and reindex with array_values() so the consuming loop cannot hit missing offsets - e2e: clear the plugin log before asserting the rejection entry so the test cannot pass on entries left by earlier tests; add regression tests for the string-account-entry and non-string-id payloads Co-Authored-By: Claude Fable 5 --- .../services/class-rop-linkedin-service.php | 14 ++++++++-- .../dashboard/linkedin-error-handling.spec.js | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/includes/admin/services/class-rop-linkedin-service.php b/includes/admin/services/class-rop-linkedin-service.php index 1b06c74d9..991a01639 100644 --- a/includes/admin/services/class-rop-linkedin-service.php +++ b/includes/admin/services/class-rop-linkedin-service.php @@ -846,7 +846,7 @@ public function add_account_with_app( $accounts_data ) { $the_id = unserialize( base64_decode( $accounts_data['id'] ) ); $accounts_array = unserialize( base64_decode( $accounts_data['pages'] ) ); - if ( empty( $the_id ) || ! is_array( $accounts_array ) ) { + if ( empty( $the_id ) || ! is_string( $the_id ) || ! is_array( $accounts_array ) ) { $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); return false; } @@ -854,11 +854,21 @@ public function add_account_with_app( $accounts_data ) { // last array item contains notify date $notify_user_at = array_pop( $accounts_array ); - if ( empty( $accounts_array ) || ! is_array( $notify_user_at ) || ! isset( $notify_user_at['notify_user_at'] ) ) { + if ( empty( $accounts_array ) || ! is_array( $notify_user_at ) || ! isset( $notify_user_at['notify_user_at'] ) || ! is_numeric( $notify_user_at['notify_user_at'] ) ) { $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); return false; } + // every remaining item must be a complete account entry, otherwise reading its fields below can fatal + foreach ( $accounts_array as $account_data ) { + if ( ! is_array( $account_data ) || ! isset( $account_data['id'], $account_data['img'], $account_data['account'], $account_data['is_company'], $account_data['user'], $account_data['access_token'] ) || ! is_string( $account_data['id'] ) || '' === $account_data['id'] ) { + $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); + return false; + } + } + + $accounts_array = array_values( $accounts_array ); + // save timestamp for when to notify user to refresh their linkedin token // set notified count to 0 $notify_data = array( diff --git a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js index 918e65b31..12d2e3a6f 100644 --- a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js +++ b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js @@ -11,6 +11,10 @@ const VALID_PAGES = 'YToyOntpOjA7YTo2OntzOjI6ImlkIjtzOjIxOiJ1cm46bGk6cGVyc29uOkU const PAGES_WITHOUT_NOTIFY = 'YToyOntpOjA7YTo2OntzOjI6ImlkIjtzOjIxOiJ1cm46bGk6cGVyc29uOkUyRVRFU1QiO3M6MzoiaW1nIjtzOjA6IiI7czo3OiJhY2NvdW50IjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEwOiJpc19jb21wYW55IjtiOjA7czo0OiJ1c2VyIjtzOjEzOiJFMkUgVGVzdCBVc2VyIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO3M6MTQ6ImUyZS10ZXN0LXRva2VuIjt9aToxO2E6MTp7czoxMDoidW5leHBlY3RlZCI7aToxO319'; // base64( serialize( [ [ 'notify_user_at' => ... ] ] ) ) — notify entry only, no accounts. const PAGES_ONLY_NOTIFY = 'YToxOntpOjA7YToxOntzOjE0OiJub3RpZnlfdXNlcl9hdCI7aTo0MTAyNDQ0ODAwO319'; +// base64( serialize( [ 'bad-account', [ 'notify_user_at' => 4102444800 ] ] ) ) — account entry is a string, not an array. +const PAGES_WITH_STRING_ACCOUNT = 'YToyOntpOjA7czoxMToiYmFkLWFjY291bnQiO2k6MTthOjE6e3M6MTQ6Im5vdGlmeV91c2VyX2F0IjtpOjQxMDI0NDQ4MDA7fX0='; +// base64( serialize( [ 'urn:li:person:E2ETEST' ] ) ) — id decodes to an array instead of a string. +const ARRAY_ID = 'YToxOntpOjA7czoyMToidXJuOmxpOnBlcnNvbjpFMkVURVNUIjt9'; /** * Call a plugin API endpoint from the dashboard page. @@ -119,7 +123,31 @@ test.describe( 'LinkedIn error handling (issue #1098)', () => { expect( serviceNames ).not.toContain( 'linkedin' ); } ); + test( 'account entry that is not an array is rejected', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: VALID_ID, + pages: PAGES_WITH_STRING_ACCOUNT, + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + } ); + + test( 'id that does not decode to a string is rejected', async ( { page } ) => { + const response = await callRopApi( page, 'add_account_li', { + id: ARRAY_ID, + pages: VALID_PAGES, + } ); + + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + } ); + test( 'rejected payload leaves the LinkedIn error in the plugin log', async ( { page } ) => { + // Earlier tests in this spec log the same entry — clear the log first + // so the assertion can only be satisfied by this request. + await callRopApi( page, 'get_log', { force: true } ); + await callRopApi( page, 'add_account_li', { id: VALID_ID, pages: btoa( 'linkedin-error-string-not-account-data' ), From 25e6034f5f2fd1873564cb84e1d6a30629a7f594 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Mon, 3 Aug 2026 12:12:54 +0300 Subject: [PATCH 5/7] test: build the ROP api URL from ropApiSettings.root safely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e utils that came in with #1094 enable pretty permalinks in the test site, so `rest_url()` — and therefore `ropApiSettings.root` — no longer carries a query string. Appending `&req=...` to it produced `/wp-json/tweet-old-post/v8/api/&req=add_account_li`, which the REST server resolved to nothing: all 10 LinkedIn tests failed with `rest_no_route` / 404 after merging development. Let URL/searchParams place `?` or `&`, the way the plugin's own `fetchAJAX` passes `req` through vue-resource's `params` option. Works under either permalink mode. Co-Authored-By: Claude Opus 5 (1M context) --- tests/e2e/specs/dashboard/linkedin-error-handling.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js index 12d2e3a6f..820f4dd25 100644 --- a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js +++ b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js @@ -25,7 +25,12 @@ const ARRAY_ID = 'YToxOntpOjA7czoyMToidXJuOmxpOnBlcnNvbjpFMkVURVNUIjt9'; */ async function callRopApi( page, req, body ) { return await page.evaluate( async ( { req, body } ) => { - const response = await fetch( `${window.ropApiSettings.root}&req=${req}`, { + // `root` carries a query string only on plain permalinks; let URL sort + // out `?` vs `&` the way the plugin's own `params` option does. + const url = new URL( window.ropApiSettings.root, window.location.href ); + url.searchParams.set( 'req', req ); + + const response = await fetch( url.toString(), { method: 'POST', body: JSON.stringify( body ), headers: { From edc94b6137f5d2964422e2f9c42253ae20593db7 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Mon, 3 Aug 2026 14:31:58 +0300 Subject: [PATCH 6/7] fix: reject array-valued LinkedIn id/pages before decoding them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review feedback on #1100. `is_set_not_empty()` deliberately accepts array values — `is_valid_serialize_data()` maps over them — so a payload of `{ id: [], pages: [] }` cleared the guard and reached `base64_decode()`, which raises a PHP 8 TypeError. That is the same critical-error response this PR set out to remove; verified locally, the request returned HTTP 500 "There has been a critical error on this website" without the new check. Guard both encoded fields as strings before decoding, and cover the payload with an e2e test that fails without it. Also reset the services store in `beforeEach`. The happy-path test registers a LinkedIn account and only cleans up on success, so with CI retries enabled a failed run left that account behind and assertions like `not.toContain('linkedin')` could pass or fail depending on order. Uses the `ropUtils` fixture that came in with #1094 rather than a second bespoke helper. Co-Authored-By: Claude Opus 5 (1M context) --- .../services/class-rop-linkedin-service.php | 6 +++++ .../dashboard/linkedin-error-handling.spec.js | 24 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/includes/admin/services/class-rop-linkedin-service.php b/includes/admin/services/class-rop-linkedin-service.php index 991a01639..b741627cb 100644 --- a/includes/admin/services/class-rop-linkedin-service.php +++ b/includes/admin/services/class-rop-linkedin-service.php @@ -843,6 +843,12 @@ public function add_account_with_app( $accounts_data ) { return false; } + // is_set_not_empty() accepts array values, which base64_decode() would fatal on. + if ( ! is_string( $accounts_data['id'] ) || ! is_string( $accounts_data['pages'] ) ) { + $this->logger->alert_error( 'Linkedin Error: received malformed account data, the account was not added.' ); + return false; + } + $the_id = unserialize( base64_decode( $accounts_data['id'] ) ); $accounts_array = unserialize( base64_decode( $accounts_data['pages'] ) ); diff --git a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js index 820f4dd25..405ab53d2 100644 --- a/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js +++ b/tests/e2e/specs/dashboard/linkedin-error-handling.spec.js @@ -1,7 +1,4 @@ -/** - * WordPress dependencies - */ -import { test, expect } from '@wordpress/e2e-test-utils-playwright'; +import { test, expect } from '../../fixtures'; // base64( serialize( 'urn:li:person:E2ETEST' ) ) const VALID_ID = 'czoyMToidXJuOmxpOnBlcnNvbjpFMkVURVNUIjs='; @@ -53,7 +50,11 @@ async function callRopApi( page, req, body ) { test.describe( 'LinkedIn error handling (issue #1098)', () => { - test.beforeEach( async ( { page, admin } ) => { + test.beforeEach( async ( { page, admin, ropUtils } ) => { + // Start every scenario with no service registered. The happy-path test + // adds a LinkedIn account and its own cleanup does not run when it + // fails, so a retry would otherwise inherit that account. + await ropUtils.reset(); await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); await page.waitForSelector( '.tab-view[type="accounts"]' ); } ); @@ -78,6 +79,19 @@ test.describe( 'LinkedIn error handling (issue #1098)', () => { await expect( page.getByRole( 'button', { name: 'LinkedIn' } ) ).toBeVisible(); } ); + test( 'array-valued id and pages are rejected without a fatal error', async ( { page } ) => { + // `is_set_not_empty()` accepts arrays, so these reach `base64_decode()` + // and used to raise a PHP 8 TypeError before the string guard. + const response = await callRopApi( page, 'add_account_li', { + id: [ VALID_ID ], + pages: [ VALID_PAGES ], + } ); + + expect( response.text ).not.toContain( 'critical error' ); + expect( response.status ).toBe( 200 ); + expect( response.body.code ).toBe( '400' ); + } ); + test( 'garbled pages payload is rejected without a fatal error', async ( { page } ) => { const response = await callRopApi( page, 'add_account_li', { id: VALID_ID, From fc2e945fb38cd22fb3ca3a12ecdf42bc1d392c04 Mon Sep 17 00:00:00 2001 From: lucadobrescu Date: Mon, 3 Aug 2026 14:40:54 +0300 Subject: [PATCH 7/7] fix: unregister the auth message listener on a malformed payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review feedback on #1100. Both failure branches this PR added to getChildWindowMessage() are terminal — the popup has already answered — but only the parsed-error one detached the global `message` handler. After a malformed payload the component stayed subscribed to the auth origin and would route any later message using the stale `modal.serviceName`. Detach in the parse branch too, matching the adjacent branch. Co-Authored-By: Claude Opus 5 (1M context) --- vue/src/vue-elements/sign-in-btn.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/vue/src/vue-elements/sign-in-btn.vue b/vue/src/vue-elements/sign-in-btn.vue index 8c4d8a828..f329a3bcd 100644 --- a/vue/src/vue-elements/sign-in-btn.vue +++ b/vue/src/vue-elements/sign-in-btn.vue @@ -897,6 +897,7 @@ export default { accountData = JSON.parse(event.data); } catch (e) { this.is_loading = false; + window.removeEventListener("message", this.getChildWindowMessage ); Vue.$log.error('Received a malformed message from the authorization window', e); return; }