diff --git a/src/Admin.php b/src/Admin.php index eae2b08..e859bd9 100644 --- a/src/Admin.php +++ b/src/Admin.php @@ -7,7 +7,7 @@ */ namespace Shazzad\PluginUpdater; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -16,14 +16,28 @@ /** * Class Admin * - * Handles plugin update checks, license verification, and upgrade processes. + * Renders the license management admin page and handles license save/verify via POST. * * @since 1.0 */ class Admin { + /** + * Integration instance holding shared state and API helpers. + * + * @since 1.0 + * + * @var Integration + */ public Integration $integration; + /** + * Constructor. + * + * @since 1.0 + * + * @param Integration $integration Integration instance. + */ public function __construct( Integration $integration ) { $this->integration = $integration; @@ -38,7 +52,7 @@ public function __construct( Integration $integration ) { */ public function admin_menu() { if ( empty( $this->integration->menu_label ) ) { - $this->integration->menu_label = sprintf( '%s Updates', $this->integration->product_name ); + $this->integration->menu_label = \sprintf( '%s Updates', $this->integration->product_name ); } if ( empty( $this->integration->menu_parent ) ) { @@ -64,63 +78,67 @@ public function admin_menu() { * @return void */ public function load_page() { + if ( ! isset( $_POST['wprepo_update'] ) ) { + return; + } + + check_admin_referer( 'wprepo_license_update' ); + $base_url = remove_query_arg( [ 'm' ] ); - if ( isset( $_POST['wprepo_update'] ) ) { - check_admin_referer( 'wprepo_license_update' ); - if ( empty( $_POST['wprepo_license'] ) ) { - delete_option( $this->integration->get_license_option() ); - $this->integration->clear_updates_transient(); - wp_redirect( - add_query_arg( - 'message', - urlencode( 'License deactivated' ), - $base_url - ) - ); - } else { - $key = sanitize_text_field( $_POST['wprepo_license'] ); - $response = $this->integration->api_request( 'check_license', $key ); - - if ( is_wp_error( $response ) ) { - $message = $response->get_error_message(); - wp_redirect( - add_query_arg( - 'error', - urlencode( $message ), - $base_url - ) - ); - } elseif ( ! empty( $response['license'] ) ) { - update_option( $this->integration->get_license_option(), $key ); - update_option( - $this->integration->license_name . '_data', - $response['license'] - ); + if ( empty( $_POST['wprepo_license'] ) ) { + delete_option( $this->integration->get_license_code_key() ); + $this->integration->clear_updates_transient(); + wp_redirect( + add_query_arg( + 'message', + urlencode( 'License deactivated' ), + $base_url + ) + ); + exit; + } - $this->integration->refresh_updates_transient(); - wp_redirect( - add_query_arg( - 'message', - urlencode( 'License activated' ), - $base_url - ) - ); - } else { - $message = ! empty( $response['message'] ) - ? $response['message'] - : 'Invalid License Key'; - wp_redirect( - add_query_arg( - 'error', - urlencode( $message ), - $base_url - ) - ); - } - } + $key = sanitize_text_field( $_POST['wprepo_license'] ); + $response = $this->integration->api_request( 'check_license', $key ); + + if ( is_wp_error( $response ) ) { + wp_redirect( + add_query_arg( + 'error', + urlencode( $response->get_error_message() ), + $base_url + ) + ); + exit; + } + + if ( ! empty( $response['license'] ) ) { + update_option( $this->integration->get_license_code_key(), $key ); + $this->integration->update_license_data( $response['license'] ); + + $this->integration->refresh_updates_transient(); + wp_redirect( + add_query_arg( + 'message', + urlencode( 'License activated' ), + $base_url + ) + ); exit; } + + $message = ! empty( $response['message'] ) + ? $response['message'] + : 'Invalid License Key'; + wp_redirect( + add_query_arg( + 'error', + urlencode( $message ), + $base_url + ) + ); + exit; } /** @@ -134,7 +152,7 @@ public function admin_page() {

integration->product_name, $this->integration->product_version @@ -143,17 +161,7 @@ public function admin_page() {

%s

', - esc_html( urldecode( $_GET['message'] ) ) - ); - } elseif ( ! empty( $_GET['error'] ) ) { - printf( - '

%s

', - esc_html( urldecode( $_GET['error'] ) ) - ); - } + $this->render_notices(); ?>
@@ -187,128 +195,168 @@ class="regular-text" /> integration->api_request( 'details' ); - if ( $this->integration->has_license_code() ) { - if ( is_wp_error( $response ) ) { - printf( - '
%s
', - $response->get_error_message() - ); - } else { - $details = $response['details']; - $output = ''; - - if ( - isset( $details['version'] ) && - version_compare( $details['version'], $this->integration->product_version, '>' ) - ) { - $output .= sprintf( - '

Upgrade available. New version %s

', - $details['version'] - ); - if ( ! empty( $details['changelog_new'] ) ) { - $output .= sprintf( - '

Changelog:

%s
', - wpautop( $details['changelog_new'] ) - ); - } - if ( ! empty( $details['upgrade_notice_new'] ) ) { - $output .= sprintf( - '

Upgrade notice:

%s
', - wpautop( $details['upgrade_notice_new'] ) - ); - } - if ( ! empty( $details['download_link'] ) ) { - $update_url = wp_nonce_url( - admin_url( - 'update.php?action=upgrade-plugin&plugin=' - . urlencode( $this->integration->product_file ) - ), - 'upgrade-plugin_' . $this->integration->product_file - ); - - $output .= sprintf( - '
-

Upgrade Now:

- Upgrade Now -
', - $update_url - ); - } else { - $license_data = get_option( $this->integration->license_name . '_data' ); - if ( - ! empty( $license_data['status'] ) - && 'expired' === $license_data['status'] - ) { - $output .= 'Your license has expired. Please renew your license to get updates.'; - } elseif ( - ! empty( $license_data['status'] ) - && 'suspended' === $license_data['status'] - ) { - $output .= 'Your license has been suspended. Please contact support.'; - } else { - $output .= 'Unable to upgrade. Please contact support.'; - } - } - } else { - $license_data = get_option( $this->integration->license_name . '_data' ); - $output .= '
You are using the latest version of our plugin.
'; - - if ( - ! empty( $license_data['status'] ) - && 'expired' === $license_data['status'] - ) { - $output .= '

Your license has expired. Please renew your license to get new updates.

'; - } - } - - printf( - '
%s
', - $output - ); - } - } elseif ( is_wp_error( $response ) ) { - printf( + if ( is_wp_error( $response ) ) { + \printf( '
%s
', $response->get_error_message() ); + } elseif ( $this->integration->has_license_code() && ! empty( $response['details'] ) ) { + $this->render_details_with_license( $response['details'] ); } elseif ( ! empty( $response['details'] ) ) { - // No license code set yet. - $details = $response['details']; - $output = ''; - - if ( - isset( $details['version'] ) && - version_compare( $details['version'], $this->integration->product_version, '>' ) - ) { - $output .= sprintf( - '

Upgrade available. New version %s

', - $details['version'] - ); - if ( ! empty( $details['changelog_new'] ) ) { - $output .= sprintf( - '

Changelog:

%s
', - wpautop( $details['changelog_new'] ) - ); - } - if ( ! empty( $details['upgrade_notice_new'] ) ) { - $output .= sprintf( - '

Upgrade notice:

%s
', - wpautop( $details['upgrade_notice_new'] ) + $this->render_details_without_license( $response['details'] ); + } + ?> + +

%s

', + esc_html( urldecode( $_GET['message'] ) ) + ); + } elseif ( ! empty( $_GET['error'] ) ) { + \printf( + '

%s

', + esc_html( urldecode( $_GET['error'] ) ) + ); + } + } + + /** + * Render upgrade version heading, changelog, and upgrade notice. + * + * @since 1.0 + * + * @param array $details Plugin details from API response. + * @return string HTML output. + */ + private function render_upgrade_info( $details ) { + $output = \sprintf( + '

Upgrade available. New version %s

', + $details['version'] + ); + if ( ! empty( $details['changelog_new'] ) ) { + $output .= \sprintf( + '

Changelog:

%s
', + wpautop( $details['changelog_new'] ) + ); + } + if ( ! empty( $details['upgrade_notice_new'] ) ) { + $output .= \sprintf( + '

Upgrade notice:

%s
', + wpautop( $details['upgrade_notice_new'] ) + ); + } + + return $output; + } + + /** + * Render plugin details panel when a license code is present. + * + * @since 1.0 + * + * @param array $details Plugin details from API response. + * @return void + */ + private function render_details_with_license( $details ) { + $output = ''; + + if ( + isset( $details['version'] ) && + version_compare( $details['version'], $this->integration->product_version, '>' ) + ) { + $output .= $this->render_upgrade_info( $details ); + + if ( ! empty( $details['download_link'] ) ) { + $update_url = wp_nonce_url( + admin_url( + 'update.php?action=upgrade-plugin&plugin=' + . urlencode( $this->integration->product_file ) + ), + 'upgrade-plugin_' . $this->integration->product_file + ); + + $output .= \sprintf( + '
+

Upgrade Now:

+ Upgrade Now +
', + $update_url + ); + } else { + if ( 'expired' === $this->integration->get_license_status() ) { + $renewal_url = $this->integration->get_license_renewal_url(); + if ( $renewal_url ) { + $output .= \sprintf( + 'Your license has expired. Renew your license to get updates.', + esc_url( $renewal_url ) ); + } else { + $output .= 'Your license has expired. Please renew your license to get updates.'; } - $output .= 'Please save your license to receive updates.'; + } elseif ( 'suspended' === $this->integration->get_license_status() ) { + $output .= 'Your license has been suspended. Please contact support.'; } else { - $output .= '
You are using the latest version of our plugin.
'; + $output .= 'Unable to upgrade. Please contact support.'; } + } + } else { + $output .= '
You are using the latest version of our plugin.
'; - printf( - '
%s
', - $output - ); + if ( 'expired' === $this->integration->get_license_status() ) { + $renewal_url = $this->integration->get_license_renewal_url(); + if ( $renewal_url ) { + $output .= \sprintf( + '

Your license has expired. Renew your license to get new updates.

', + esc_url( $renewal_url ) + ); + } else { + $output .= '

Your license has expired. Please renew your license to get new updates.

'; + } } - ?> - - %s', + $output + ); + } + + /** + * Render plugin details panel when no license code is set. + * + * @since 1.0 + * + * @param array $details Plugin details from API response. + * @return void + */ + private function render_details_without_license( $details ) { + $output = ''; + + if ( + isset( $details['version'] ) && + version_compare( $details['version'], $this->integration->product_version, '>' ) + ) { + $output .= $this->render_upgrade_info( $details ); + $output .= 'Please save your license to receive updates.'; + } else { + $output .= '
You are using the latest version of our plugin.
'; + } + + \printf( + '
%s
', + $output + ); } } diff --git a/src/Integration.php b/src/Integration.php index 007b784..8023904 100644 --- a/src/Integration.php +++ b/src/Integration.php @@ -9,7 +9,7 @@ use WP_Error; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -18,7 +18,8 @@ /** * Class Integration * - * Handles plugin update checks, license verification, and upgrade processes. + * Main entry point for consumer plugins. Holds all shared state (API URL, product + * info, license config) and provides API request and license/transient helpers. * * @since 1.0 */ @@ -165,13 +166,13 @@ public function __construct( } /** - * Retrieves the option name for storing the license key. + * Retrieves the option key for storing the license code. * * @since 1.0 * * @return string */ - public function get_license_option() { + public function get_license_code_key() { return "{$this->license_name}_code"; } @@ -183,7 +184,7 @@ public function get_license_option() { * @return false|string License code or false if not found. */ public function get_license_code() { - return get_option( $this->get_license_option() ); + return get_option( $this->get_license_code_key() ); } /** @@ -197,6 +198,82 @@ public function has_license_code() { return (bool) $this->get_license_code(); } + /** + * Retrieves the option key for storing license data. + * + * @since 1.0 + * + * @return string + */ + public function get_license_data_key() { + return "{$this->license_name}_data"; + } + + + /** + * Get license status. + * + * @return string + */ + public function get_license_status() { + $data = $this->get_license_data(); + + if ( ! empty( $data['status'] ) ) { + return $data['status']; + } + + return 'unknown'; + } + + /** + * Get license renewal URL from stored license data. + * + * @since 1.0 + * + * @return string Renewal URL or empty string if not available. + */ + public function get_license_renewal_url() { + $data = $this->get_license_data(); + + if ( empty( $data['renewal_url'] ) ) { + return ''; + } + + $url = str_replace( + [ '{license_code}', '{email}' ], + [ + $this->get_license_code() ? $this->get_license_code() : '', + ! empty( $data['email'] ) ? $data['email'] : '', + ], + $data['renewal_url'] + ); + + return $url; + } + + /** + * Gets the license data from the database. + * + * @since 1.0 + * + * @return false|array License data or false if not found. + */ + public function get_license_data() { + return get_option( $this->get_license_data_key() ); + } + + /** + * Updates the license data in the database. + * + * @since 1.0 + * + * @param array $data License data to store. + * @return bool True if the value was updated, false otherwise. + */ + public function update_license_data( $data ) { + return update_option( $this->get_license_data_key(), $data ); + } + /** * Checks if the license is currently active. * @@ -205,12 +282,7 @@ public function has_license_code() { * @return bool True if license is active, false otherwise. */ public function is_license_active() { - $data = get_option( "{$this->license_name}_data" ); - if ( empty( $data ) ) { - return false; - } - - if ( isset( $data['status'] ) && 'active' === $data['status'] ) { + if ( 'active' === $this->get_license_status() ) { return true; } @@ -277,7 +349,7 @@ public function clear_updates_transient() { public function api_request( $method, $license = '' ) { $request_url = "{$this->api_url}/products/{$this->product_id}/$method"; - $args = [ + $args = [ 'product_version' => $this->product_version, 'product_status' => $this->product_status, 'wp_url' => esc_url( site_url( '', 'https' ) ), diff --git a/src/Tracker.php b/src/Tracker.php index b9e5a6f..b881b77 100644 --- a/src/Tracker.php +++ b/src/Tracker.php @@ -7,7 +7,7 @@ */ namespace Shazzad\PluginUpdater; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -16,14 +16,28 @@ /** * Class Tracker * - * Handles plugin update checks, license verification, and upgrade processes. + * Handles plugin activation/deactivation hooks and hourly cron license sync. * * @since 1.0 */ class Tracker { + /** + * Integration instance holding shared state and API helpers. + * + * @since 1.0 + * + * @var Integration + */ public Integration $integration; + /** + * Constructor. + * + * @since 1.0 + * + * @param Integration $integration Integration instance. + */ public function __construct( Integration $integration ) { $this->integration = $integration; @@ -56,7 +70,7 @@ public function sync_license_data() { } if ( ! empty( $response['license'] ) ) { - update_option( $this->integration->license_name . '_data', $response['license'] ); + $this->integration->update_license_data( $response['license'] ); } } diff --git a/src/Updater.php b/src/Updater.php index 495e815..86b3f98 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -7,7 +7,7 @@ */ namespace Shazzad\PluginUpdater; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -16,14 +16,28 @@ /** * Class Updater * - * Handles plugin update checks, license verification, and upgrade processes. + * Hooks into the WordPress update system to inject update data from the remote API. * * @since 1.0 */ class Updater { + /** + * Integration instance holding shared state and API helpers. + * + * @since 1.0 + * + * @var Integration + */ public Integration $integration; + /** + * Constructor. + * + * @since 1.0 + * + * @param Integration $integration Integration instance. + */ public function __construct( Integration $integration ) { $this->integration = $integration; @@ -205,7 +219,7 @@ public function upgrader_process_complete( $upgrader, $args ) { $args['type'] === 'plugin' && $args['action'] === 'update' && isset( $args['plugins'] ) && - in_array( $this->integration->product_file, $args['plugins'] ) + \in_array( $this->integration->product_file, $args['plugins'] ) ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugin = get_plugin_data( WP_PLUGIN_DIR . '/' . $this->integration->product_file ); diff --git a/tests/IntegrationLicenseTest.php b/tests/IntegrationLicenseTest.php index 2518ba3..de70550 100644 --- a/tests/IntegrationLicenseTest.php +++ b/tests/IntegrationLicenseTest.php @@ -6,21 +6,21 @@ class IntegrationLicenseTest extends TestCase { /** @test */ - public function get_license_option_returns_expected_key() { + public function get_license_code_key_returns_expected_key() { $integration = $this->create_integration(); // license_name is sanitize_key( "my-plugin42" ) = "my-plugin42" - $this->assertSame( 'my-plugin42_code', $integration->get_license_option() ); + $this->assertSame( 'my-plugin42_code', $integration->get_license_code_key() ); } /** @test */ - public function get_license_option_varies_by_product() { + public function get_license_code_key_varies_by_product() { $integration = $this->create_integration( [ 'product_file' => 'other-plugin/other-plugin.php', 'product_id' => '99', ] ); - $this->assertSame( 'other-plugin99_code', $integration->get_license_option() ); + $this->assertSame( 'other-plugin99_code', $integration->get_license_code_key() ); } /** @test */ @@ -130,4 +130,105 @@ public function is_license_active_returns_false_when_status_missing() { $this->assertFalse( $integration->is_license_active() ); } + + /** @test */ + public function get_license_renewal_url_replaces_placeholders() { + $integration = $this->create_integration(); + + Functions\when( 'get_option' )->alias( function ( $key ) { + if ( 'my-plugin42_data' === $key ) { + return [ + 'renewal_url' => 'https://example.com/renew?license={license_code}&email={email}', + 'email' => 'user@example.com', + ]; + } + if ( 'my-plugin42_code' === $key ) { + return 'ABC-123-DEF'; + } + return false; + } ); + + $this->assertSame( + 'https://example.com/renew?license=ABC-123-DEF&email=user@example.com', + $integration->get_license_renewal_url() + ); + } + + /** @test */ + public function get_license_renewal_url_returns_empty_when_no_url() { + $integration = $this->create_integration(); + + Functions\expect( 'get_option' ) + ->with( 'my-plugin42_data' ) + ->andReturn( [ 'status' => 'expired' ] ); + + $this->assertSame( '', $integration->get_license_renewal_url() ); + } + + /** @test */ + public function get_license_renewal_url_handles_missing_email() { + $integration = $this->create_integration(); + + Functions\when( 'get_option' )->alias( function ( $key ) { + if ( 'my-plugin42_data' === $key ) { + return [ + 'renewal_url' => 'https://example.com/renew?license={license_code}&email={email}', + ]; + } + if ( 'my-plugin42_code' === $key ) { + return 'ABC-123-DEF'; + } + return false; + } ); + + $this->assertSame( + 'https://example.com/renew?license=ABC-123-DEF&email=', + $integration->get_license_renewal_url() + ); + } + + /** @test */ + public function get_license_renewal_url_handles_missing_license_code() { + $integration = $this->create_integration(); + + Functions\when( 'get_option' )->alias( function ( $key ) { + if ( 'my-plugin42_data' === $key ) { + return [ + 'renewal_url' => 'https://example.com/renew?license={license_code}&email={email}', + 'email' => 'user@example.com', + ]; + } + if ( 'my-plugin42_code' === $key ) { + return false; + } + return false; + } ); + + $this->assertSame( + 'https://example.com/renew?license=&email=user@example.com', + $integration->get_license_renewal_url() + ); + } + + /** @test */ + public function get_license_renewal_url_returns_url_without_placeholders() { + $integration = $this->create_integration(); + + Functions\when( 'get_option' )->alias( function ( $key ) { + if ( 'my-plugin42_data' === $key ) { + return [ + 'renewal_url' => 'https://example.com/renew', + ]; + } + if ( 'my-plugin42_code' === $key ) { + return 'ABC-123-DEF'; + } + return false; + } ); + + $this->assertSame( + 'https://example.com/renew', + $integration->get_license_renewal_url() + ); + } }