diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f2ce16c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Build plugin release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate version and build ZIP + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + VERSION="${RELEASE_TAG#v}" + grep -q "Version: ${VERSION}$" dabestaniha-authenticate-bridge.php + mkdir -p build/mahak-authenticate-bridge dist + rsync -a \ + --exclude='.git/' \ + --exclude='.github/' \ + --exclude='build/' \ + --exclude='dist/' \ + --exclude='test/' \ + ./ build/mahak-authenticate-bridge/ + cd build + zip -qr ../dist/mahak-authenticate-bridge.zip mahak-authenticate-bridge + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: gh release create "$RELEASE_TAG" dist/mahak-authenticate-bridge.zip --generate-notes --title "$RELEASE_TAG" diff --git a/README.md b/README.md index 45d4e11..6e16fca 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ Authenticate WordPress user via json web token from Laravel Application. Debug mode is disabled by default. An administrator can enable it from **Settings > Mahak Authentication** while troubleshooting. Because diagnostic pages expose server paths and other technical details to visitors of the login URL, disable it again after diagnosing the problem. +## Updates + +Administrators can check for and install published releases from **Settings > Mahak Authentication**. Updates also appear in WordPress's standard **Plugins** and **Updates** screens. + +To publish an update, change the plugin `Version` header and `MAHAK_AUTHENTICATE_BRIDGE_VERSION` constant to the same version, commit the change, and push a matching tag such as `v2.3.0` to GitHub. The release workflow builds and attaches the ZIP required by the WordPress updater. + ## Usage Send users to: diff --git a/dabestaniha-authenticate-bridge.php b/dabestaniha-authenticate-bridge.php index 9067735..7c45649 100644 --- a/dabestaniha-authenticate-bridge.php +++ b/dabestaniha-authenticate-bridge.php @@ -2,9 +2,11 @@ /** * Plugin Name: Mahak Authenticate Bridge + * Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Description: Authenticate WordPress user via json web token from Mahakiha Application. - * Version: 2.1.0 + * Version: 2.2.0 * Requires PHP: 7.4 + * Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Author: Dabestaniha * * Example: https://wordpress.test/mahak/login/?token=eyJ0... @@ -14,6 +16,10 @@ use Dabestaniha\AuthenticateBridge\Autoloader; use Dabestaniha\AuthenticateBridge\App\Http\Controllers\AuthenticateController; use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageMenuController; use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageFormController; +use Dabestaniha\AuthenticateBridge\App\Support\PluginUpdater; + +define('MAHAK_AUTHENTICATE_BRIDGE_VERSION', '2.2.0'); +define('MAHAK_AUTHENTICATE_BRIDGE_FILE', __FILE__); require_once __DIR__.'/src/Autoloader.php'; @@ -24,6 +30,8 @@ Autoloader::register([ require_once __DIR__.'/src/helpers.php'; +PluginUpdater::boot(); + add_action('init', fn() => AuthenticateController::resolve()); add_action('admin_menu', fn() => SettingsPageMenuController::resolve()); diff --git a/src/app/Http/Controllers/SettingsPageMenuController.php b/src/app/Http/Controllers/SettingsPageMenuController.php index 1616b07..ba6bdf1 100644 --- a/src/app/Http/Controllers/SettingsPageMenuController.php +++ b/src/app/Http/Controllers/SettingsPageMenuController.php @@ -2,6 +2,8 @@ namespace Dabestaniha\AuthenticateBridge\App\Http\Controllers; +use Dabestaniha\AuthenticateBridge\App\Support\PluginUpdater; + class SettingsPageMenuController extends InvocableController { public function __invoke() @@ -27,7 +29,8 @@ class SettingsPageMenuController extends InvocableController submit_button(); ?> + plugin = plugin_basename(MAHAK_AUTHENTICATE_BRIDGE_FILE); + $this->slug = dirname($this->plugin); + } + + public static function boot(): void + { + if (self::$instance !== null) { + return; + } + + self::$instance = new self(); + add_filter('pre_set_site_transient_update_plugins', [self::$instance, 'addUpdate']); + add_filter('plugins_api', [self::$instance, 'addPluginInformation'], 20, 3); + add_action('admin_post_'.self::CHECK_ACTION, [self::$instance, 'checkNow']); + } + + public function addUpdate($transient) + { + if (!is_object($transient) || empty($transient->checked)) { + return $transient; + } + + $update = $this->availableUpdate(); + + if ($update !== null) { + if (!isset($transient->response) || !is_array($transient->response)) { + $transient->response = []; + } + + $transient->response[$this->plugin] = $update; + } + + return $transient; + } + + public function addPluginInformation($result, string $action, $args) + { + if ($action !== 'plugin_information' || ($args->slug ?? '') !== $this->slug) { + return $result; + } + + $release = $this->latestRelease(); + + if (is_wp_error($release)) { + return $result; + } + + $package = $this->releasePackage($release); + + if ($package === null) { + return $result; + } + + return (object) [ + 'name' => 'Mahak Authenticate Bridge', + 'slug' => $this->slug, + 'version' => $this->releaseVersion($release), + 'author' => 'Dabestaniha', + 'homepage' => $release['html_url'] ?? 'https://github.com/dabestaniha/mahak-authenticate-bridge', + 'download_link' => $package, + 'requires_php' => '7.4', + 'sections' => [ + 'description' => 'Authenticate WordPress users via JSON Web Tokens from the Mahakiha application.', + 'changelog' => nl2br(esc_html((string) ($release['body'] ?? ''))), + ], + ]; + } + + public function checkNow(): void + { + if (!current_user_can('update_plugins')) { + wp_die(esc_html__('Sorry, you are not allowed to update plugins.')); + } + + check_admin_referer(self::CHECK_ACTION); + delete_site_transient(self::RELEASE_CACHE); + delete_site_transient('update_plugins'); + wp_update_plugins(); + + wp_safe_redirect(add_query_arg( + ['page' => config('mahak.settings-page'), 'update-checked' => '1'], + admin_url('options-general.php') + )); + exit; + } + + public static function renderUpdatePanel(): void + { + if (self::$instance === null || !current_user_can('update_plugins')) { + return; + } + + $updater = self::$instance; + $release = $updater->latestRelease(); + + echo '

'.esc_html__('Plugin updates').'

'; + echo '

'.sprintf( + esc_html__('Installed version: %s'), + ''.esc_html(MAHAK_AUTHENTICATE_BRIDGE_VERSION).'' + ).'

'; + + if (is_wp_error($release)) { + echo '

'.esc_html($release->get_error_message()).'

'; + } else { + $update = $updater->availableUpdate($release); + + if ($update !== null) { + echo '

'.sprintf(esc_html__('Version %s is available.'), esc_html($update->new_version)).'

'; + $updateUrl = wp_nonce_url( + self_admin_url('update.php?action=upgrade-plugin&plugin='.rawurlencode($updater->plugin)), + 'upgrade-plugin_'.$updater->plugin + ); + echo '

'.esc_html__('Update now').'

'; + } else { + echo '

'.esc_html__('You are using the latest available version.').'

'; + } + } + + echo '
'; + echo ''; + wp_nonce_field(self::CHECK_ACTION); + submit_button(esc_html__('Check for updates'), 'secondary', 'submit', false); + echo '
'; + } + + private function availableUpdate(?array $release = null): ?object + { + $release = $release ?? $this->latestRelease(); + + if (is_wp_error($release)) { + return null; + } + + $version = $this->releaseVersion($release); + $package = $this->releasePackage($release); + + if ($version === '' || $package === null || !version_compare($version, MAHAK_AUTHENTICATE_BRIDGE_VERSION, '>')) { + return null; + } + + return (object) [ + 'id' => $release['html_url'] ?? self::API_URL, + 'slug' => $this->slug, + 'plugin' => $this->plugin, + 'new_version' => $version, + 'url' => $release['html_url'] ?? '', + 'package' => $package, + 'requires_php' => '7.4', + ]; + } + + private function latestRelease() + { + $cached = get_site_transient(self::RELEASE_CACHE); + + if (is_array($cached)) { + return $cached; + } + + $response = wp_remote_get(self::API_URL, [ + 'headers' => [ + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'Mahak-Authenticate-Bridge/'.MAHAK_AUTHENTICATE_BRIDGE_VERSION, + ], + 'timeout' => 10, + ]); + + if (is_wp_error($response)) { + return new \WP_Error('mahak_update_request_failed', 'Could not contact GitHub to check for plugin updates.'); + } + + if (wp_remote_retrieve_response_code($response) !== 200) { + return new \WP_Error('mahak_update_response_invalid', 'No published plugin release is currently available on GitHub.'); + } + + $release = json_decode(wp_remote_retrieve_body($response), true); + + if (!is_array($release) || !empty($release['draft']) || !empty($release['prerelease'])) { + return new \WP_Error('mahak_update_release_invalid', 'GitHub returned invalid plugin release information.'); + } + + if ($this->releaseVersion($release) === '' || $this->releasePackage($release) === null) { + return new \WP_Error( + 'mahak_update_asset_missing', + 'The latest GitHub release does not contain a valid mahak-authenticate-bridge.zip package.' + ); + } + + set_site_transient(self::RELEASE_CACHE, $release, 6 * HOUR_IN_SECONDS); + + return $release; + } + + private function releaseVersion(array $release): string + { + return ltrim((string) ($release['tag_name'] ?? ''), 'vV'); + } + + private function releasePackage(array $release): ?string + { + foreach (($release['assets'] ?? []) as $asset) { + if (($asset['name'] ?? '') !== self::RELEASE_ASSET) { + continue; + } + + $url = esc_url_raw((string) ($asset['browser_download_url'] ?? '')); + + $path = (string) wp_parse_url($url, PHP_URL_PATH); + + if ( + $url !== '' + && wp_parse_url($url, PHP_URL_SCHEME) === 'https' + && wp_parse_url($url, PHP_URL_HOST) === 'github.com' + && strpos($path, '/dabestaniha/mahak-authenticate-bridge/releases/download/') === 0 + ) { + return $url; + } + } + + return null; + } +} diff --git a/test/integration-authentication.php b/test/integration-authentication.php index 45a612a..2a6c294 100644 --- a/test/integration-authentication.php +++ b/test/integration-authentication.php @@ -278,6 +278,50 @@ function test_debug_error_details(): void pass('Debug mode displays full exception details and stack trace'); } +function test_plugin_update_discovery(): void +{ + $packageUrl = 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/download/v2.3.0/mahak-authenticate-bridge.zip'; + $mockRelease = function ($response, array $request, string $url) use ($packageUrl) { + if ($url !== 'https://api.github.com/repos/dabestaniha/mahak-authenticate-bridge/releases/latest') { + return $response; + } + + return [ + 'headers' => [], + 'body' => json_encode([ + 'tag_name' => 'v2.3.0', + 'html_url' => 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/tag/v2.3.0', + 'body' => 'Test release', + 'draft' => false, + 'prerelease' => false, + 'assets' => [[ + 'name' => 'mahak-authenticate-bridge.zip', + 'browser_download_url' => $packageUrl, + ]], + ], JSON_THROW_ON_ERROR), + 'response' => ['code' => 200, 'message' => 'OK'], + 'cookies' => [], + 'filename' => null, + ]; + }; + + delete_site_transient('mahak_authenticate_bridge_release'); + add_filter('pre_http_request', $mockRelease, 10, 3); + + $updates = apply_filters('pre_set_site_transient_update_plugins', (object) [ + 'checked' => [TEST_PLUGIN => MAHAK_AUTHENTICATE_BRIDGE_VERSION], + 'response' => [], + ]); + + remove_filter('pre_http_request', $mockRelease, 10); + + assert_true(isset($updates->response[TEST_PLUGIN]), 'A newer GitHub release was not offered as a WordPress update.'); + assert_true($updates->response[TEST_PLUGIN]->new_version === '2.3.0', 'The offered plugin version was incorrect.'); + assert_true($updates->response[TEST_PLUGIN]->package === $packageUrl, 'The release package URL was incorrect.'); + + pass('Published GitHub releases are discovered by the WordPress updater'); +} + boot_wordpress(); install_wordpress(); activate_and_configure_plugin(); @@ -285,5 +329,6 @@ wait_for_http(); test_valid_authentication(); test_invalid_authentication(); test_debug_error_details(); +test_plugin_update_discovery(); echo "[OK] Authentication integration test passed\n";