From 99f78228603ad42133e5b60d6a6656e76c46eaa1 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 1 Jul 2026 21:19:43 +0100 Subject: [PATCH 1/2] Added in a new global excludes rule system so we cna exclude domains at source --- .claude/settings.json | 15 ++ .wp-env.json | 3 +- e2e/mu-plugins/iawmlf-e2e-exclusion.php | 112 ++++++++++++ e2e/specs/link-details-exclusion.spec.js | 94 ++++++++++ src/Action/Link_Check_Action.php | 10 + src/Link/Link_Exclusion.php | 73 +++++--- src/Settings/Settings.php | 25 +++ templates/admin/reports/link-details.php | 18 +- tests/Action/Test_Link_Check_Action.php | 90 +++++++++ tests/Link/Test_Link_Exclusion.php | 172 ++++++++++++++++++ tests/Link/Test_Link_Repository_Exclusion.php | 72 ++++++++ .../Test_Settings_Exclusion_Sources.php | 49 +++++ tests/WP_Post/Test_WP_Post_Controller.php | 19 -- 13 files changed, 703 insertions(+), 49 deletions(-) create mode 100644 .claude/settings.json create mode 100644 e2e/mu-plugins/iawmlf-e2e-exclusion.php create mode 100644 e2e/specs/link-details-exclusion.spec.js create mode 100644 tests/Action/Test_Link_Check_Action.php create mode 100644 tests/Link/Test_Link_Exclusion.php create mode 100644 tests/Link/Test_Link_Repository_Exclusion.php create mode 100644 tests/Settings/Test_Settings_Exclusion_Sources.php diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..a19ab06f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(ls -lh ~/realesrgan/models/RealESRGAN_x4plus.pth)", + "Read(//home/glynn/realesrgan/models/**)", + "Read(//mnt/karkinos/karkinos-setup/app/public/assets/bg/**)", + "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs/wp-content/plugins/internet-archive-wayback-machine-link-fixer \"./vendor/bin/phpunit --colors=never 2>&1 | tail -70\")", + "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs 'wp eval '\\\\''$r = new \\\\Internet_Archive\\\\Wayback_Machine_Link_Fixer\\\\Link\\\\Link_Repository\\(\\); $l = $r->find_by_id\\( 6220 \\); $l->set_excluded\\( false \\); $r->upsert\\( $l \\); var_dump\\( $r->find_by_id\\( 6220 \\)->is_excluded\\(\\) \\);'\\\\'' 2>&1')", + "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs \"wp option update iawmlf_link_exclusions '[\\\\\"*.twitter.*\\\\\",\\\\\"dfsdfsd\\\\\",\\\\\"ffff\\\\\"]' --format=json 2>&1; echo '--- now:'; wp option get iawmlf_link_exclusions --format=json 2>&1\")" + ], + "additionalDirectories": [ + "/home/glynn/realesrgan" + ] + } +} diff --git a/.wp-env.json b/.wp-env.json index 4ecdd216..ac14cdcf 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -3,7 +3,8 @@ "phpVersion": "8.3", "plugins": [ "." ], "mappings": { - "wp-content/mu-plugins/iawmlf-e2e-non200.php": "./e2e/mu-plugins/iawmlf-e2e-non200.php" + "wp-content/mu-plugins/iawmlf-e2e-non200.php": "./e2e/mu-plugins/iawmlf-e2e-non200.php", + "wp-content/mu-plugins/iawmlf-e2e-exclusion.php": "./e2e/mu-plugins/iawmlf-e2e-exclusion.php" }, "port": 57893, "testsPort": 57894, diff --git a/e2e/mu-plugins/iawmlf-e2e-exclusion.php b/e2e/mu-plugins/iawmlf-e2e-exclusion.php new file mode 100644 index 00000000..34b7b164 --- /dev/null +++ b/e2e/mu-plugins/iawmlf-e2e-exclusion.php @@ -0,0 +1,112 @@ + settings list -> built-in list): + * + * - built-in layer : an option-gated `iawmlf_bundled_link_exclusions` filter (the real list is + * a code constant, so this is the only runtime way to toggle it). + * - settings layer : the spec flips the normal `iawmlf_link_exclusions` option directly. + * - per-link flag : `wp iawmlf-e2e-exclusion flag <0|1>`. + * + * Commands: + * wp iawmlf-e2e-exclusion seed -> seeds one broken, un-excluded link, prints LINK_ID. + * wp iawmlf-e2e-exclusion flag … -> sets that link's per-link excluded flag. + * wp iawmlf-e2e-exclusion cleanup -> removes the link + all e2e exclusion state. + * + * @package Internet_Archive\Wayback_Machine_Link_Fixer\E2E + */ + +defined( 'ABSPATH' ) || exit; + +use Internet_Archive\Wayback_Machine_Link_Fixer\Settings\Settings; +use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Repository; + +const IAWMLF_E2E_EXCL_URL = 'https://example.com/iawmlf-e2e-panel-link'; +const IAWMLF_E2E_BUILTIN_OPT = 'iawmlf_e2e_builtin_exclusion'; + +// Built-in list layer, toggled by an option so the spec can flip it via wp-cli. +add_filter( + 'iawmlf_bundled_link_exclusions', + function ( array $list ): array { + if ( get_option( IAWMLF_E2E_BUILTIN_OPT ) ) { + $list[] = '*iawmlf-e2e-panel-link*'; + } + return $list; + } +); + +if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { + return; +} + +/** + * Remove the seeded link and all e2e exclusion state (idempotent). + * + * @return void + */ +function iawmlf_e2e_exclusion_cleanup(): void { + global $wpdb; + delete_option( IAWMLF_E2E_BUILTIN_OPT ); + update_option( Settings::LINK_EXCLUSIONS, array() ); + $wpdb->delete( Settings::get_link_table_name(), array( 'url' => IAWMLF_E2E_EXCL_URL ), array( '%s' ) ); +} + +// Seed one broken, un-excluded link and mark onboarding complete so admin pages render. +WP_CLI::add_command( + 'iawmlf-e2e-exclusion seed', + function (): void { + global $wpdb; + + update_option( Settings::POST_ACTIVATION_ONBOARDING_KEY, Settings::ONBOARDING_COMPLETED_OPTION ); + update_option( Settings::SETUP_WIZARD_COMPLETED_KEY, true ); + update_option( Settings::SETUP_WIZARD_STEP_KEY, 'complete' ); + + iawmlf_e2e_exclusion_cleanup(); + + $wpdb->insert( + Settings::get_link_table_name(), + array( + 'url' => IAWMLF_E2E_EXCL_URL, + 'archived' => 'https://web.archive.org/web/2024/' . IAWMLF_E2E_EXCL_URL, + 'checks' => wp_json_encode( array( array( 'date' => gmdate( 'Y-m-d H:i:s' ), 'http_code' => 404 ) ) ), + 'message' => '', + 'redirect_url' => '', + 'is_broken' => 1, + 'excluded' => 0, + 'archive_process' => 'done', + ), + array( '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s' ) + ); + + echo 'LINK_ID=' . (int) $wpdb->insert_id . "\n"; + } +); + +// Toggle the per-link excluded flag: wp iawmlf-e2e-exclusion flag <0|1>. +WP_CLI::add_command( + 'iawmlf-e2e-exclusion flag', + function ( array $args ): void { + $id = isset( $args[0] ) ? (int) $args[0] : 0; + $on = isset( $args[1] ) && '0' !== $args[1]; + + $repo = new Link_Repository(); + $link = $repo->find_by_id( $id ); + if ( $link ) { + $link->set_excluded( $on ); + $repo->upsert( $link ); + } + + echo 'FLAG=' . ( $on ? '1' : '0' ) . "\n"; + } +); + +WP_CLI::add_command( + 'iawmlf-e2e-exclusion cleanup', + function (): void { + iawmlf_e2e_exclusion_cleanup(); + echo "CLEANED=1\n"; + } +); diff --git a/e2e/specs/link-details-exclusion.spec.js b/e2e/specs/link-details-exclusion.spec.js new file mode 100644 index 00000000..a486cf9b --- /dev/null +++ b/e2e/specs/link-details-exclusion.spec.js @@ -0,0 +1,94 @@ +const { test, expect } = require( '@playwright/test' ); +const { execSync } = require( 'child_process' ); +const path = require( 'path' ); + +/** + * Link-details exclusion priority — the "unwind" test. + * + * One link is excluded by all three layers at once, then peeled back one at a time. The "Link + * Exclusion" panel must walk the priority order as each layer is removed: + * + * all three on -> per-link flag wins the message ("currently excluded"), checkbox read-only + * remove flag -> settings list ("excluded by your exclusion settings list"), read-only + * remove settings-> built-in list ("excluded by the built-in exclusion list"), read-only + * remove built-in-> nothing ("Excluding this link will stop it…"), editable checkbox + * + * Layers are toggled via the iawmlf-e2e-exclusion mu-plugin (per-link flag + option-gated + * built-in filter) and the normal settings option. + */ + +const PLUGIN_DIR = 'wp-content/plugins/internet-archive-wayback-machine-link-fixer'; +const REPO_ROOT = path.join( __dirname, '../..' ); + +function wpCli( command ) { + return execSync( + `npx wp-env run cli --env-cwd='${ PLUGIN_DIR }' -- ${ command }`, + { cwd: REPO_ROOT, encoding: 'utf8' } + ); +} + +function seedLink() { + const output = wpCli( 'wp iawmlf-e2e-exclusion seed' ); + const match = output.match( /^LINK_ID=(\d+)$/m ); + if ( ! match ) { + throw new Error( `Seeder did not print LINK_ID. Output was:\n${ output }` ); + } + return match[ 1 ]; +} + +const setPerLinkFlag = ( linkId, on ) => wpCli( `wp iawmlf-e2e-exclusion flag ${ linkId } ${ on ? 1 : 0 }` ); +const setSettingsList = ( jsonArray ) => wpCli( `wp option update iawmlf_link_exclusions '${ jsonArray }' --format=json` ); +const setBuiltInList = ( on ) => ( on + ? wpCli( 'wp option update iawmlf_e2e_builtin_exclusion 1' ) + : wpCli( 'wp option delete iawmlf_e2e_builtin_exclusion' ) ); + +const PATTERN = '["*iawmlf-e2e-panel-link*"]'; + +test.describe( 'link details exclusion priority', () => { + let linkId; + let detailsPath; + + test.beforeAll( () => { + linkId = seedLink(); + detailsPath = `/wp-admin/admin.php?page=iawmlf-links&iawmlf_link_id=${ linkId }`; + } ); + + test.afterAll( () => { + setPerLinkFlag( linkId, false ); + setSettingsList( '[]' ); + setBuiltInList( false ); + wpCli( 'wp iawmlf-e2e-exclusion cleanup' ); + } ); + + test( 'all three on, then peeled back one at a time, walk the priority order', async ( { page } ) => { + const panel = page.locator( '#iawmlf_link_exclusion .inside' ); + const toggle = page.locator( '#iawmlf_toggle_exclusion' ); + + // 1) All three layers on: the per-link flag wins the message; checkbox is read-only. + setPerLinkFlag( linkId, true ); + setSettingsList( PATTERN ); + setBuiltInList( true ); + await page.goto( detailsPath ); + await expect( panel ).toContainText( 'currently excluded' ); + await expect( toggle ).toBeDisabled(); + + // 2) Remove the per-link flag: the settings list now owns it. + setPerLinkFlag( linkId, false ); + await page.goto( detailsPath ); + await expect( panel ).toContainText( 'excluded by your exclusion settings list' ); + await expect( toggle ).toBeDisabled(); + + // 3) Remove the settings list: the built-in list now owns it. + setSettingsList( '[]' ); + await page.goto( detailsPath ); + await expect( panel ).toContainText( 'excluded by the built-in exclusion list' ); + await expect( toggle ).toBeDisabled(); + + // 4) Remove the built-in list: nothing excludes it — the checkbox is editable again. + setBuiltInList( false ); + await page.goto( detailsPath ); + await expect( panel ).toContainText( 'Excluding this link will stop it' ); + await expect( toggle ).toBeEnabled(); + await expect( toggle ).toHaveAttribute( 'name', 'iawmlf_exclude_link' ); + } ); +} ); diff --git a/src/Action/Link_Check_Action.php b/src/Action/Link_Check_Action.php index 83c04406..bbc11352 100644 --- a/src/Action/Link_Check_Action.php +++ b/src/Action/Link_Check_Action.php @@ -14,6 +14,7 @@ use Throwable; use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link; +use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Exclusion; use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Repository; defined( 'ABSPATH' ) || exit; @@ -90,6 +91,15 @@ public function check_link( int $link_id ): array { ); } + // If the link matches our exclusion list (built-in or settings), skip checking it. + if ( Link_Exclusion::get_instance()->is_excluded( $link ) ) { + return array( + 'link' => $link, + 'checked' => false, + 'valid' => true, + ); + } + // Get the current status. try { $status = $this->link_checker->check_single( $link->get_href() ); diff --git a/src/Link/Link_Exclusion.php b/src/Link/Link_Exclusion.php index 42bc8f2d..26e5eb5c 100644 --- a/src/Link/Link_Exclusion.php +++ b/src/Link/Link_Exclusion.php @@ -15,28 +15,32 @@ defined( 'ABSPATH' ) || exit; /** - * Handles the exclusion of links. + * Handles the exclusion of links against the built-in and user exclusion lists. */ class Link_Exclusion { /** - * Holds the global exclusions. + * The built-in exclusion patterns. * * @var string[] */ - private static $exclusions = null; + private $bundled; + /** + * The user settings exclusion patterns. + * + * @var string[] + */ + private $settings; /** - * Create an instance of the class. + * Create an instance of the class, loading both exclusion lists. * * @return void */ public function __construct() { - // If we have not loaded the exclusions, load them. - if ( null === self::$exclusions ) { - self::$exclusions = get_option( Settings::LINK_EXCLUSIONS, array() ); - } + $this->bundled = Settings::get_bundled_link_exclusions(); + $this->settings = Settings::get_link_exclusions(); } /** @@ -49,7 +53,7 @@ public static function get_instance(): Link_Exclusion { } /** - * Checks if a give link is excluded. + * Checks if a given link is excluded by either the built-in or the settings list. * * @param Link $link The link to check. * @param integer|null $post_id The post ID to check. @@ -57,42 +61,61 @@ public static function get_instance(): Link_Exclusion { * @return boolean */ public function is_excluded( Link $link, ?int $post_id = null ): bool { + $excluded = $this->is_globally_excluded( $link ) || $this->is_settings_excluded( $link ); + return null !== $post_id - ? apply_filters( 'iawmlf_exclude_link_from_post', $this->is_global_excluded( $link ), $link, $post_id ) - : $this->is_global_excluded( $link ); + ? apply_filters( 'iawmlf_exclude_link_from_post', $excluded, $link, $post_id ) + : $excluded; } /** - * Filters an array of links to check for exclusions. + * Checks if a link is in the built-in exclusion list. * - * @param Link[] $links The links to check. - * @param integer|null $post_id The post ID to check. + * @param Link $link The link to check. * - * @return Link[] + * @return boolean */ - public function filter_excluded( array $links, ?int $post_id = null ): array { - return array_filter( - $links, - function ( Link $link ) use ( $post_id ): bool { - return ! $this->is_global_excluded( $link, $post_id ); + public function is_globally_excluded( Link $link ): bool { + foreach ( $this->bundled as $pattern ) { + if ( fnmatch( $pattern, $link->get_href() ) ) { + return true; } - ); + } + + return false; } /** - * Checks if a link is excluded from the global exclusions. + * Checks if a link is in the user settings exclusion list. * * @param Link $link The link to check. * * @return boolean */ - private function is_global_excluded( Link $link ): bool { - foreach ( self::$exclusions as $ex ) { - if ( fnmatch( $ex, $link->get_href() ) ) { + public function is_settings_excluded( Link $link ): bool { + foreach ( $this->settings as $pattern ) { + if ( fnmatch( $pattern, $link->get_href() ) ) { return true; } } return false; } + + /** + * Filters an array of links to remove excluded ones. + * + * @param Link[] $links The links to check. + * @param integer|null $post_id The post ID to check. + * + * @return Link[] + */ + public function filter_excluded( array $links, ?int $post_id = null ): array { + return array_filter( + $links, + function ( Link $link ) use ( $post_id ): bool { + return ! $this->is_excluded( $link, $post_id ); + } + ); + } } diff --git a/src/Settings/Settings.php b/src/Settings/Settings.php index 97268008..154109ac 100644 --- a/src/Settings/Settings.php +++ b/src/Settings/Settings.php @@ -73,6 +73,17 @@ class Settings { public const LINK_ICON = self::SETTINGS_PREFIX . 'link_icon'; public const LINK_ICON_NONE = 'none'; + /** + * Built-in list of exclusion patterns bundled with the plugin, merged with the user's settings + * list when matching. Extendable via the undocumented `iawmlf_bundled_link_exclusions` filter. + * + * @var string[] + */ + public const BUNDLED_LINK_EXCLUSIONS = array( + '*.linkedin.com*', // LinkedIn sub-domains (www.linkedin.com etc.) — blocks the Internet Archive link checker. + '*//linkedin.com*', // LinkedIn apex (https://linkedin.com/...). + ); + /** * Gets the link table name. * @@ -167,6 +178,20 @@ public static function get_link_exclusions(): array { return apply_filters( 'iawmlf_link_exclusions', $links ); } + /** + * Get the built-in exclusion list bundled with the plugin. + * + * Merged with the user's settings list when matching. Extendable via the undocumented + * `iawmlf_bundled_link_exclusions` filter. + * + * @since 1.5.0 + * + * @return string[] + */ + public static function get_bundled_link_exclusions(): array { + return (array) apply_filters( 'iawmlf_bundled_link_exclusions', self::BUNDLED_LINK_EXCLUSIONS ); + } + /** * Get the array of excluded post IDs for the link fixer. * diff --git a/templates/admin/reports/link-details.php b/templates/admin/reports/link-details.php index 85005804..b67f3b32 100644 --- a/templates/admin/reports/link-details.php +++ b/templates/admin/reports/link-details.php @@ -12,7 +12,7 @@ defined( 'ABSPATH' ) || exit; use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link; -use Internet_Archive\Wayback_Machine_Link_Fixer\Settings\Settings; +use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Exclusion; // Check if we have any previous links to show. $iawmlf_check_count = count( $iawmlf_link->get_checks() ); @@ -20,6 +20,7 @@ // Generate the title. $iawmlf_link_title = iawmlf_trim_string( str_replace( array( 'http://', 'https://' ), '', $iawmlf_link->get_href() ), 55 ); + ?>

@@ -91,6 +92,10 @@
is_excluded() ) : ?>

+ is_settings_excluded( $iawmlf_link ) ) : ?> +

+ is_globally_excluded( $iawmlf_link ) ) : ?> +

@@ -98,10 +103,15 @@
diff --git a/tests/Action/Test_Link_Check_Action.php b/tests/Action/Test_Link_Check_Action.php new file mode 100644 index 00000000..0f775396 --- /dev/null +++ b/tests/Action/Test_Link_Check_Action.php @@ -0,0 +1,90 @@ +wpdb = $GLOBALS['wpdb']; + $this->link_repository = new Link_Repository(); + + $this->wpdb->query( 'TRUNCATE TABLE ' . Settings::get_link_table_name() ); + delete_option( Settings::LINK_EXCLUSIONS ); + remove_all_filters( 'iawmlf_link_checker_client' ); + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + + parent::set_up(); + } + + public function tear_down(): void { + delete_option( Settings::LINK_EXCLUSIONS ); + remove_all_filters( 'iawmlf_link_checker_client' ); + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + + parent::tear_down(); + } + + /** + * @testdox A link matched by the exclusion list is treated as valid without ever calling the checker. + * + * @return void + */ + public function test_list_excluded_link_does_not_call_checker(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*blocked.example*' ) ); + + $link = $this->link_repository->upsert( new Link( 'https://blocked.example/x' ) ); + + $checker = $this->createMock( Link_Checker_Client::class ); + $checker->method( 'is_online' )->willReturn( true ); + $checker->expects( $this->never() )->method( 'check_single' ); + add_filter( 'iawmlf_link_checker_client', fn() => $checker ); + + $action = new Link_Check_Action(); + $result = $action->check_link( $link->get_id() ); + + $this->assertFalse( $result['checked'], 'A list-excluded link should not be checked.' ); + $this->assertTrue( $result['valid'], 'A list-excluded link is treated as valid.' ); + } + + /** + * @testdox A link that matches no exclusion list is checked as normal. + * + * @return void + */ + public function test_non_excluded_link_is_checked(): void { + $link = $this->link_repository->upsert( new Link( 'https://allowed.example/x' ) ); + + $checker = $this->createMock( Link_Checker_Client::class ); + $checker->method( 'is_online' )->willReturn( true ); + $checker->expects( $this->once() )->method( 'check_single' )->willReturn( 200 ); + add_filter( 'iawmlf_link_checker_client', fn() => $checker ); + + $action = new Link_Check_Action(); + $result = $action->check_link( $link->get_id() ); + + $this->assertTrue( $result['checked'] ); + } +} diff --git a/tests/Link/Test_Link_Exclusion.php b/tests/Link/Test_Link_Exclusion.php new file mode 100644 index 00000000..0b998daf --- /dev/null +++ b/tests/Link/Test_Link_Exclusion.php @@ -0,0 +1,172 @@ +reset(); + } + + public function tear_down(): void { + $this->reset(); + parent::tear_down(); + } + + private function reset(): void { + delete_option( Settings::LINK_EXCLUSIONS ); + remove_all_filters( 'iawmlf_link_exclusions' ); + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + } + + private function is_excluded( string $href ): bool { + return Link_Exclusion::get_instance()->is_excluded( new Link( $href ) ); + } + + /** + * @testdox A built-in (bundled) pattern excludes matching links. + * + * @return void + */ + public function test_bundled_pattern_excludes(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*.domain.tld*' ) ); + + $this->assertTrue( $this->is_excluded( 'https://www.domain.tld/x' ) ); + $this->assertTrue( $this->is_excluded( 'https://sub.domain.tld/' ) ); + } + + /** + * @testdox Matching is anchored fnmatch: `*.domain.tld*` matches sub-domains but not the bare apex, and a host pattern needs surrounding wildcards to match a full URL. + * + * @return void + */ + public function test_matching_is_anchored_fnmatch(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*.domain.tld*' ) ); + $this->assertFalse( $this->is_excluded( 'https://domain.tld/' ) ); + + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( 'pics.google.com*' ) ); + $this->assertFalse( $this->is_excluded( 'https://pics.google.com/x' ), 'Needs *pics.google.com* to match a full URL.' ); + } + + /** + * @testdox A stored Settings list pattern excludes matching links. + * + * @return void + */ + public function test_settings_option_excludes(): void { + update_option( Settings::LINK_EXCLUSIONS, array( '*example.org*' ) ); + + $this->assertTrue( $this->is_excluded( 'https://example.org/x' ) ); + } + + /** + * @testdox The documented iawmlf_link_exclusions filter affects matching. + * + * @return void + */ + public function test_documented_settings_filter_affects_matching(): void { + add_filter( + 'iawmlf_link_exclusions', + function ( array $links ): array { + $links[] = '*example.com*'; + return $links; + } + ); + + $this->assertTrue( $this->is_excluded( 'https://example.com/page' ) ); + } + + /** + * @testdox The built-in list and the settings list are both consulted (merged). + * + * @return void + */ + public function test_built_in_and_settings_are_merged(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*builtin.test*' ) ); + update_option( Settings::LINK_EXCLUSIONS, array( '*settings.test*' ) ); + + $this->assertTrue( $this->is_excluded( 'https://builtin.test/a' ) ); + $this->assertTrue( $this->is_excluded( 'https://settings.test/a' ) ); + $this->assertFalse( $this->is_excluded( 'https://neither.test/a' ) ); + } + + /** + * @testdox A link matched by both lists stays excluded until it is removed from both (union). + * + * @return void + */ + public function test_union_and_removal(): void { + $href = 'https://pics.google.com/x'; + + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*.google.com*' ) ); + update_option( Settings::LINK_EXCLUSIONS, array( '*pics.google.com*' ) ); + $this->assertTrue( $this->is_excluded( $href ) ); + + // Remove the built-in list -> still excluded by the settings list. + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + $this->assertTrue( $this->is_excluded( $href ) ); + + // Remove the settings list too -> no longer excluded. + update_option( Settings::LINK_EXCLUSIONS, array() ); + $this->assertFalse( $this->is_excluded( $href ) ); + } + + /** + * @testdox A link matching no list is not excluded. + * + * @return void + */ + public function test_unmatched_not_excluded(): void { + $this->assertFalse( $this->is_excluded( 'https://not-listed.example/x' ) ); + } + + /** + * @testdox is_globally_excluded is true only for links matched by the built-in list. + * + * @return void + */ + public function test_is_globally_excluded(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*builtin.test*' ) ); + update_option( Settings::LINK_EXCLUSIONS, array( '*settings.test*' ) ); + + $exclusion = Link_Exclusion::get_instance(); + $this->assertTrue( $exclusion->is_globally_excluded( new Link( 'https://builtin.test/x' ) ) ); + $this->assertFalse( $exclusion->is_globally_excluded( new Link( 'https://settings.test/x' ) ) ); + $this->assertFalse( $exclusion->is_globally_excluded( new Link( 'https://neither.test/x' ) ) ); + } + + /** + * @testdox is_settings_excluded is true only for links matched by the settings list. + * + * @return void + */ + public function test_is_settings_excluded(): void { + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*builtin.test*' ) ); + update_option( Settings::LINK_EXCLUSIONS, array( '*settings.test*' ) ); + + $exclusion = Link_Exclusion::get_instance(); + $this->assertTrue( $exclusion->is_settings_excluded( new Link( 'https://settings.test/x' ) ) ); + $this->assertFalse( $exclusion->is_settings_excluded( new Link( 'https://builtin.test/x' ) ) ); + $this->assertFalse( $exclusion->is_settings_excluded( new Link( 'https://neither.test/x' ) ) ); + } +} diff --git a/tests/Link/Test_Link_Repository_Exclusion.php b/tests/Link/Test_Link_Repository_Exclusion.php new file mode 100644 index 00000000..081b034d --- /dev/null +++ b/tests/Link/Test_Link_Repository_Exclusion.php @@ -0,0 +1,72 @@ +wpdb = $GLOBALS['wpdb']; + $this->link_repository = new Link_Repository(); + + $this->wpdb->query( 'TRUNCATE TABLE ' . Settings::get_link_table_name() ); + delete_option( Settings::LINK_EXCLUSIONS ); + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + + parent::set_up(); + } + + public function tear_down(): void { + delete_option( Settings::LINK_EXCLUSIONS ); + remove_all_filters( 'iawmlf_bundled_link_exclusions' ); + + parent::tear_down(); + } + + /** + * @testdox A link whose own excluded flag is false but which matches a list rule is still excluded from retrieval. + * + * @return void + */ + public function test_list_matched_link_is_excluded_even_when_flag_is_false(): void { + $post_id = self::factory()->post->create(); + + // Not flagged on the link itself (simulates a user who un-checked "Exclude this link"). + $link = $this->link_repository->upsert( new Link( 'https://blocked.example/x' ) ); + $this->assertFalse( $link->is_excluded() ); + + update_post_meta( $post_id, Settings::LINK_META_KEY, array( $link->get_id() ) ); + + // The link matches a built-in list rule. + add_filter( 'iawmlf_bundled_link_exclusions', fn(): array => array( '*blocked.example*' ) ); + + // With exclusion filtering on, the list wins: the link is skipped. + $filtered = $this->link_repository->get_links_for_post( $post_id, true ); + $this->assertTrue( $filtered->is_empty(), 'A list-matched link must be excluded from retrieval regardless of its own flag.' ); + + // With filtering off, the link is still returned. + $unfiltered = $this->link_repository->get_links_for_post( $post_id, false ); + $this->assertCount( 1, $unfiltered->get_links() ); + } +} diff --git a/tests/Settings/Test_Settings_Exclusion_Sources.php b/tests/Settings/Test_Settings_Exclusion_Sources.php new file mode 100644 index 00000000..cb8d175c --- /dev/null +++ b/tests/Settings/Test_Settings_Exclusion_Sources.php @@ -0,0 +1,49 @@ +assertContains( '*.linkedin.com*', Settings::get_bundled_link_exclusions() ); + + add_filter( + 'iawmlf_bundled_link_exclusions', + function ( array $list ): array { + $list[] = '*blocked.test*'; + return $list; + } + ); + + $filtered = Settings::get_bundled_link_exclusions(); + $this->assertContains( '*.linkedin.com*', $filtered, 'The bundled default is kept.' ); + $this->assertContains( '*blocked.test*', $filtered, 'The filter can add more.' ); + } +} diff --git a/tests/WP_Post/Test_WP_Post_Controller.php b/tests/WP_Post/Test_WP_Post_Controller.php index b6d76f4b..6aabe1d6 100644 --- a/tests/WP_Post/Test_WP_Post_Controller.php +++ b/tests/WP_Post/Test_WP_Post_Controller.php @@ -12,7 +12,6 @@ namespace Internet_Archive\Wayback_Machine_Link_Fixer\Tests\Processor; use Internet_Archive\Wayback_Machine_Link_Fixer\Settings\Settings; -use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Exclusion; use Internet_Archive\Wayback_Machine_Link_Fixer\Link\Link_Repository; use Internet_Archive\Wayback_Machine_Link_Fixer\WP_Post\WP_Post_Controller; @@ -630,12 +629,6 @@ public function test_excluded_link_not_included_in_render_block_data(): void { // Add a global exclusion pattern that matches one of the links. update_option( Settings::LINK_EXCLUSIONS, array( '*excluded-domain.com*' ) ); - // Reset the Link_Exclusion static cache so it picks up the new option. - $reflection = new \ReflectionClass( Link_Exclusion::class ); - $property = $reflection->getProperty( 'exclusions' ); - $property->setAccessible( true ); - $property->setValue( null, null ); - $post_id = self::factory()->post->create(); // Add content with two links — one that will be excluded, one that won't. @@ -672,9 +665,6 @@ public function test_excluded_link_not_included_in_render_block_data(): void { // Clean up. unset( $GLOBALS['post'] ); \delete_option( Settings::LINK_EXCLUSIONS ); - - // Reset the Link_Exclusion static cache. - $property->setValue( null, null ); } /** @@ -689,12 +679,6 @@ public function test_excluded_link_filtered_from_render_block_data_with_mixed_li // Add a global exclusion pattern that matches one of the links. update_option( Settings::LINK_EXCLUSIONS, array( '*excluded-domain.com*' ) ); - // Reset the Link_Exclusion static cache so it picks up the new option. - $reflection = new \ReflectionClass( Link_Exclusion::class ); - $property = $reflection->getProperty( 'exclusions' ); - $property->setAccessible( true ); - $property->setValue( null, null ); - $post_id = self::factory()->post->create(); // Add content with two links — one excluded, one allowed. @@ -734,9 +718,6 @@ public function test_excluded_link_filtered_from_render_block_data_with_mixed_li // Clean up. unset( $GLOBALS['post'] ); \delete_option( Settings::LINK_EXCLUSIONS ); - - // Reset the Link_Exclusion static cache. - $property->setValue( null, null ); } /** From 124b3f21f3c0f1bd1bda09fe135a63ce21521c99 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 1 Jul 2026 21:21:44 +0100 Subject: [PATCH 2/2] remove local claude config --- .claude/settings.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index a19ab06f..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(ls -lh ~/realesrgan/models/RealESRGAN_x4plus.pth)", - "Read(//home/glynn/realesrgan/models/**)", - "Read(//mnt/karkinos/karkinos-setup/app/public/assets/bg/**)", - "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs/wp-content/plugins/internet-archive-wayback-machine-link-fixer \"./vendor/bin/phpunit --colors=never 2>&1 | tail -70\")", - "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs 'wp eval '\\\\''$r = new \\\\Internet_Archive\\\\Wayback_Machine_Link_Fixer\\\\Link\\\\Link_Repository\\(\\); $l = $r->find_by_id\\( 6220 \\); $l->set_excluded\\( false \\); $r->upsert\\( $l \\); var_dump\\( $r->find_by_id\\( 6220 \\)->is_excluded\\(\\) \\);'\\\\'' 2>&1')", - "Bash(/media/glynn/2024/devilbox_public/perique-sandbox/scripts/devilbox-run.sh pinkcrab/htdocs \"wp option update iawmlf_link_exclusions '[\\\\\"*.twitter.*\\\\\",\\\\\"dfsdfsd\\\\\",\\\\\"ffff\\\\\"]' --format=json 2>&1; echo '--- now:'; wp option get iawmlf_link_exclusions --format=json 2>&1\")" - ], - "additionalDirectories": [ - "/home/glynn/realesrgan" - ] - } -}