diff --git a/src/Update/UpdateChecker.php b/src/Update/UpdateChecker.php
index 0e61430..1d5ac80 100644
--- a/src/Update/UpdateChecker.php
+++ b/src/Update/UpdateChecker.php
@@ -35,6 +35,43 @@ class UpdateChecker {
public function register(): void {
add_filter( 'update_plugins_' . self::HOSTNAME, [ $this, 'provideUpdate' ], 10, 3 );
+ add_filter( 'plugin_row_meta', [ $this, 'filterRowMeta' ], 10, 2 );
+ }
+
+ /**
+ * Replace core's "View details" link on the Plugins screen.
+ *
+ * Core points that link at the WordPress.org plugin-information API
+ * (`plugin-install.php?tab=plugin-information&plugin=…`), which 404s in a
+ * "Plugin not found" iframe for this off-directory plugin. We swap it for a
+ * direct link to the matching Gitea release tag page, opened in a new tab —
+ * embedding Gitea in the thickbox iframe would be blocked by its
+ * `X-Frame-Options: SAMEORIGIN` anyway.
+ *
+ * @param mixed $meta Row-meta links for the plugin.
+ * @param mixed $plugin_file Plugin file the meta belongs to.
+ * @return mixed The (possibly modified) row-meta array.
+ */
+ public function filterRowMeta( mixed $meta, mixed $plugin_file ): mixed {
+ if ( ! is_array( $meta ) || plugin_basename( USC_PLUGIN_FILE ) !== $plugin_file ) {
+ return $meta;
+ }
+
+ // Drop core's WordPress.org "View details" thickbox link.
+ $meta = array_values(
+ array_filter(
+ $meta,
+ static fn( $item ): bool => ! ( is_string( $item ) && str_contains( $item, 'open-plugin-details-modal' ) )
+ )
+ );
+
+ $meta[] = sprintf(
+ '%s',
+ esc_url( self::REPO_URL . '/releases/tag/v' . USC_VERSION ),
+ esc_html__( 'View details', 'unsupervised-schedular' )
+ );
+
+ return $meta;
}
/**
diff --git a/tests/Unit/Update/UpdateCheckerTest.php b/tests/Unit/Update/UpdateCheckerTest.php
index 8bf0981..457b885 100644
--- a/tests/Unit/Update/UpdateCheckerTest.php
+++ b/tests/Unit/Update/UpdateCheckerTest.php
@@ -35,10 +35,45 @@ class UpdateCheckerTest extends TestCase
public function testRegisterHooksHostnameFilter(): void
{
Filters\expectAdded('update_plugins_git.unsupervised.ca')->once();
+ Filters\expectAdded('plugin_row_meta')->once();
(new UpdateChecker())->register();
}
+ public function testFilterRowMetaReplacesViewDetailsLinkForThisPlugin(): void
+ {
+ Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
+
+ $meta = [
+ 'Version 1.0.0',
+ 'View details',
+ ];
+
+ $result = (new UpdateChecker())->filterRowMeta($meta, self::PLUGIN_FILE);
+
+ // Core's thickbox modal link is gone.
+ foreach ($result as $item) {
+ self::assertStringNotContainsString('open-plugin-details-modal', $item);
+ }
+ // Replaced with a new-tab link to the Gitea release tag for USC_VERSION (1.0.0 in tests).
+ self::assertStringContainsString(
+ UpdateChecker::REPO_URL . '/releases/tag/v1.0.0',
+ end($result)
+ );
+ self::assertStringContainsString('target="_blank"', end($result));
+ }
+
+ public function testFilterRowMetaLeavesOtherPluginsUntouched(): void
+ {
+ Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
+
+ $meta = ['View details'];
+
+ $result = (new UpdateChecker())->filterRowMeta($meta, 'other-plugin/other-plugin.php');
+
+ self::assertSame($meta, $result);
+ }
+
public function testOffersUpdateWhenReleaseIsNewer(): void
{
Functions\when('plugin_basename')->justReturn(self::PLUGIN_FILE);
diff --git a/unsupervised-schedular.php b/unsupervised-schedular.php
index 118bd55..08c0696 100644
--- a/unsupervised-schedular.php
+++ b/unsupervised-schedular.php
@@ -1,12 +1,13 @@