diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53c9ae07e..64faeab4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,6 +69,7 @@ jobs: 'EndToEnd/forms/general', 'EndToEnd/forms/post-types', 'EndToEnd/general/other', + 'EndToEnd/general/uninstall', 'EndToEnd/general/plugin-screens', 'EndToEnd/integrations/divi-builder', 'EndToEnd/integrations/divi-theme', @@ -187,7 +188,10 @@ jobs: # DISALLOW_FILE_MODS = true is required to disable the block directory's "Available to Install" suggestions, which trips up # tests that search and wait for block results. + # We don't enable DISALLOW_FILE_MODS for the UninstallCest test, as tests will perform a Plugin deletion + # which requires DISALLOW_FILE_MODS to not be true. - name: Enable DISALLOW_FILE_MODS + if: ${{ matrix.test-groups != 'EndToEnd/general/uninstall' }} working-directory: ${{ env.ROOT_DIR }} run: | wp-cli config set DISALLOW_FILE_MODS true --raw diff --git a/tests/EndToEnd/general/other/ActivateDeactivatePluginCest.php b/tests/EndToEnd/general/other/ActivateDeactivatePluginCest.php index 88dfd4e37..82760c62a 100644 --- a/tests/EndToEnd/general/other/ActivateDeactivatePluginCest.php +++ b/tests/EndToEnd/general/other/ActivateDeactivatePluginCest.php @@ -59,5 +59,6 @@ public function testPluginActivationAndDeactivationWithOtherPlugins(EndToEndTest // Deactivate Plugins. $I->deactivateThirdPartyPlugin($I, 'convertkit-for-woocommerce'); $I->deactivateKitPlugin($I); + $I->resetKitPlugin($I); } } diff --git a/tests/EndToEnd/general/uninstall/UninstallCest.php b/tests/EndToEnd/general/uninstall/UninstallCest.php new file mode 100644 index 000000000..2ae1cc343 --- /dev/null +++ b/tests/EndToEnd/general/uninstall/UninstallCest.php @@ -0,0 +1,106 @@ +activateKitPlugin($I); + + // Generate an access token and refresh token by API key and secret. + // We don't use the tokens from the environment, as revoking those + // would result in later tests failing. + $result = wp_remote_post( + 'https://api.kit.com/wordpress/accounts/oauth_access_token', + [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode( + [ + 'api_key' => $_ENV['CONVERTKIT_API_KEY'], + 'api_secret' => $_ENV['CONVERTKIT_API_SECRET'], + 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + 'tenant_name' => wp_generate_password( 10, false ), // Random tenant name to produce a token for this request only. + ] + ), + ] + ); + $tokens = json_decode(wp_remote_retrieve_body($result), true)['oauth']; + + // Store the tokens and API keys in the Plugin's settings. + $I->setupKitPlugin( + $I, + [ + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'], + 'token_expires' => $tokens['expires_at'], + 'api_key' => $_ENV['CONVERTKIT_API_KEY'], + 'api_secret' => $_ENV['CONVERTKIT_API_SECRET'], + ] + ); + + // Deactivate the Plugin. + $I->deactivateKitPlugin($I); + + // Delete the Plugin. + $I->deleteKitPlugin($I); + + // Confirm the credentials have been removed from the Plugin's settings. + $I->wait(3); + $settings = $I->grabOptionFromDatabase('_wp_convertkit_settings'); + $I->assertEmpty($settings['access_token']); + $I->assertEmpty($settings['refresh_token']); + $I->assertEmpty($settings['token_expires']); + $I->assertEmpty($settings['api_key']); + $I->assertEmpty($settings['api_secret']); + + // Confirm attempting to use the revoked access token no longer works. + $result = wp_remote_get( + 'https://api.kit.com/v4/account', + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $tokens['access_token'], + ], + ] + ); + $data = json_decode(wp_remote_retrieve_body($result), true); + $I->assertArrayHasKey( 'errors', $data ); + $I->assertEquals( 'The access token was revoked', $data['errors'][0] ); + + // Confirm attempting to use the revoked refresh token no longer works. + $result = wp_remote_post( + 'https://api.kit.com/v4/oauth/token', + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $tokens['access_token'], + ], + 'body' => [ + 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + 'grant_type' => 'refresh_token', + 'refresh_token' => $tokens['refresh_token'], + ], + ] + ); + $data = json_decode(wp_remote_retrieve_body($result), true); + $I->assertArrayHasKey( 'error', $data ); + $I->assertEquals( 'invalid_grant', $data['error'] ); + } +} diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index 05ce2056f..593a0bf09 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -38,6 +38,19 @@ public function deactivateKitPlugin($I) $I->deactivateThirdPartyPlugin($I, 'convertkit'); } + /** + * Helper method to delete the Kit Plugin, checking + * it deleted and no errors were output. + * + * @since 3.2.4 + * + * @param EndToEndTester $I EndToEndTester. + */ + public function deleteKitPlugin($I) + { + $I->deleteThirdPartyPlugin($I, 'convertkit'); + } + /** * Helper method to programmatically setup the Plugin's settings, as if the * user configured the Plugin at `Settings > Kit`. diff --git a/tests/Support/Helper/ThirdPartyPlugin.php b/tests/Support/Helper/ThirdPartyPlugin.php index a0ba082ac..eb1d29141 100644 --- a/tests/Support/Helper/ThirdPartyPlugin.php +++ b/tests/Support/Helper/ThirdPartyPlugin.php @@ -121,6 +121,39 @@ public function deactivateThirdPartyPlugin($I, $name) } } + /** + * Helper method to delete a third party Plugin, checking + * it deleted and no errors were output. + * + * @since 3.2.4 + * + * @param EndToEndTester $I EndToEnd Tester. + * @param string $name Plugin Slug. + */ + public function deleteThirdPartyPlugin($I, $name) + { + // Login as the Administrator, if we're not already logged in. + if ( ! $this->amLoggedInAsAdmin($I) ) { + $this->doLoginAsAdmin($I); + } + + // Go to the Plugins screen in the WordPress Administration interface. + $I->amOnPluginsPage(); + + // Wait for the Plugins page to load. + $I->waitForElementVisible('body.plugins-php'); + + // Delete the Plugin. + $I->waitForElementVisible('a#delete-' . $name); + $I->click('a#delete-' . $name); + + // Click the confirmation dialog. + $I->acceptPopup(); + + // Wait for the Plugin to be marked as deleted. + $I->waitForElementNotVisible('table.plugins tr.deleted[data-slug=' . $name . ']'); + } + /** * Helper method to check if the Administrator is logged in. * diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 000000000..ac47f35cd --- /dev/null +++ b/uninstall.php @@ -0,0 +1,75 @@ + Delete. + * + * @package ConvertKit + * @author ConvertKit + */ + +// If uninstall.php is not called by WordPress, die. +if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { + die; +} + +// Only WordPress and PHP methods can be used. Plugin classes and methods +// are not reliably available due to the Plugin being deactivated and going +// through deletion now. + +// Get settings. +$settings = get_option( '_wp_convertkit_settings' ); + +// Bail if no settings exist. +if ( ! $settings ) { + return; +} + +// Revoke Access Token. +if ( array_key_exists( 'access_token', $settings ) && ! empty( $settings['access_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'HXZlOCj-K5r0ufuWCtyoyo3f688VmMAYSsKg1eGvw0Y', + 'token' => $settings['access_token'], + ) + ), + 'timeout' => 5, + ) + ); +} + +// Revoke Refresh Token. +if ( array_key_exists( 'refresh_token', $settings ) && ! empty( $settings['refresh_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'HXZlOCj-K5r0ufuWCtyoyo3f688VmMAYSsKg1eGvw0Y', + 'token' => $settings['refresh_token'], + ) + ), + 'timeout' => 5, + ) + ); +} + +// Remove credentials from settings. +$settings['access_token'] = ''; +$settings['refresh_token'] = ''; +$settings['token_expires'] = ''; +$settings['api_key'] = ''; +$settings['api_secret'] = ''; + +// Save settings. +update_option( '_wp_convertkit_settings', $settings );