Skip to content

Releases: Yoko-Co/yoko-link-checker

v1.2.0

Choose a tag to compare

@raygunNo raygunNo released this 12 Aug 20:47
18f9791

Release Notes — Yoko Link Checker 1.2.0

Monthly maintenance: the admin screen moves, the numbers finally agree, and four security findings are closed.

Why this release

The plugin reported a different number of broken links depending on where you looked — the dashboard, the report screen and the exported spreadsheet each gave a different answer for the same site. There was no way to tell which one was right, which made the tool unusable for telling a client how much work a site needed.

That turned out not to be a bug in a query. Two screens were counting two different things and both were right: the dashboard counted unique URLs, the report counted link occurrences. One broken URL used in twelve posts is legitimately 1 and 12. They also disagreed on filters, which is what made them drift apart for good.

What's new

The counts agree, and they say what they count

Every figure now comes from the same queries, and every screen states both units — "12 broken URLs across 34 links". A URL is the problem count: you fix it once. An occurrence is the work count: each is a place someone has to open and edit. Both matter when scoping work, so neither is hidden.

Moved to Tools → Yoko Link Checker

One menu entry instead of a top-level section, with Dashboard, Reports and Settings as tabs on a single screen. Named in full throughout so it is clear whose tool it is.

WP-CLI

wp yoko-lc counts     # every status, in both units
wp yoko-lc verify     # assert the counts are consistent; non-zero exit if not
wp yoko-lc prune      # delete link rows whose source post is gone
wp yoko-lc export     # same export as the admin screen, same filters

An uninstall setting

Deleting the plugin no longer has to delete your scan data. uninstall.php had always read this option and defaulted to deleting everything, but nothing ever wrote it — there was no way to answer the question until now.

Fixed

  • "Last scan" was wrong on every site not set to UTC. Datetimes are stored in site-local time but were read as UTC, adding the site's offset to every age — a scan that had just finished read "4 hours ago" on US Eastern. The duration beside it was correct, because subtracting two equally wrong timestamps cancels the error, which is exactly what made the wrong number look credible.
  • A just-started scan could be failed as stale, from the same misreading applied to the 30-minute staleness check.
  • Searching the report advertised empty pages — the count query ignored the search term that the row query applied.
  • The error status was invisible — counted in the dashboard total but unreachable in the UI; ?status=error silently fell back to Broken.
  • Dashboard cards did not add up. Blocked, timeout and error were in the total but had no card. They now share a Needs Review card, kept separate from Broken because most blocked responses are bot protection rather than a dead link.
  • Ignoring a link desynchronised the screens permanently — the dashboard ignored the ignored flag entirely, so its numbers never moved when the report's did.
  • CSV export ignored your filters, was headed "Broken URL" while containing every status, and left Source URL and Source Title blank for pages and every custom post type.
  • Scan progress could not reach 100% — the denominator counted URLs the checker never fetches, and the completed-state branch was unreachable.
  • Link rows are pruned when a post is permanently deleted and when links are removed from content, so occurrence counts stop drifting upward forever.

Security

  • Redirects are validated at every hop. The checker validated only the URL it was given, then handed the request to the transport with up to three redirects followed unchecked — so an innocuous external link responding 302 Location: http://169.254.169.254/… was fetched. This is the class of issue WordPress fixed in core's URL validation in 7.0.3; the plugin did not inherit that fix because it bypassed wp_http_validate_url(). Four related gaps closed at the same time: DNS resolution failure was treated as safe rather than blocked, http://[::1]/ bypassed the check entirely, there was no scheme allow-list (leaving file:// and gopher:// open), and the DNS cache lived for the whole process.
  • Every AJAX endpoint has its own nonce. All ten shared one, so a token leaked from any plugin page authorised clear_data — which truncates all three tables — exactly as readily as a status poll. clear_data now requires manage_options.
  • Status polling can no longer spawn cron on demand. That endpoint needs only view capability but called spawn_cron() on every poll; it is now rate-limited.
  • Settings saving uses Post/Redirect/Get. The save ran during render, so a refresh silently re-submitted the form, including the cron reschedule.
  • Uninstall always clears scheduled events now. It previously returned early when data was being kept, leaving yoko_lc_auto_scan firing forever with no handler once the plugin was gone.

WordPress 7.0.2 and 7.0.4 were reviewed and need no plugin-side action — 7.0.2 is REST and WP_Query internals, 7.0.4 is the Imagick/Ghostscript image path, neither of which this plugin touches.

Upgrade notes

Three things will look like bugs if you are not expecting them:

  1. Anyone with a plugin page open across the upgrade gets one "Security check failed" on their next action — their page holds nonces in the old format. Reloading fixes it. Unavoidable when nonce actions change.
  2. Old admin bookmarks stop working. The screen moved from admin.php?page=yoko-link-checker* to tools.php?page=yoko-link-checker&tab=…. The menu slug itself is unchanged, so stored data and capabilities are unaffected.
  3. ylcAdmin.nonce is now ylcAdmin.nonces, keyed by action. Only affects bespoke integrations calling the AJAX endpoints directly.

Breaking change — yoko_lc_internal_http_args is removed. Internal URL checks used to run through a separate HTTP path with their own arguments and SSL verification disabled; they now go through the same client as every other request, so there is one SSRF gate rather than two. Use yoko_lc_http_request_args instead, plus yoko_lc_allow_private_urls where a private address must be reachable.

Take a database snapshot before the first scan. Running a scan deletes stale and orphaned link rows, and wp yoko-lc prune does so explicitly. Those rows are orphaned by definition and removing them is the intended fix for counts drifting upward — but the deletions are not undone by rolling back the code.

There is no schema change and no migration, so the code itself reverts cleanly.

Requires: WordPress 6.0+ (tested to 7.0) · PHP 8.0+

v1.1.1

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 17:57

v1.1.1

Release Date: March 4, 2026

Resolves 17 code review findings from Round 4 codebase review (Wave 4).

Critical Fix (P1)

  • Polling race condition — scheduleNextPoll now fires in AJAX complete callback, preventing permanent scan status stalls

Important Fixes (P2)

  • check_timeout mismatch — Default corrected from 8 to 30, matching Activator/AdminController
  • register_menu capability — Uses fixed custom strings instead of current_user_can() at registration time
  • Export handler — Moved from admin_init to load-{$hook_suffix} for page-specific execution
  • Cron reschedule — Uses time() instead of time() + interval
  • strtotime() validation — Return values validated in DashboardPage/LinksListTable
  • Media extension guard — Added for attachment_url_to_postid()
  • N+1 query optimization — Batch URL hash lookups (find_by_hashes()) and link existence checks (find_existing_batch())

Code Quality (P3)

  • Added ylcAdmin undefined guard, enforced Yoda conditions
  • Added null check on get_edit_post_link(), fixed sanitize_csv_value() return type
  • Removed duplicate do_action calls in ResultsPage ignore/unignore
  • Moved inline settings script to admin.js bindEvents()
  • Cron schedules filter registered before activation with explicit 0 args
  • url_id now passed in ignore/unignore hooks
  • Removed redundant count_posts() per batch; uses scan record total

Removed

  • Dead modal code (HTML, JS closeModal/Escape handlers, CSS rules)
  • 8 dead repository methods (~166 LOC) from LinkRepository and ScanRepository
  • Unused PROBLEM_STATUSES constant
  • next_check column and index from schema

Database

  • Schema version bumped to 1.2.0

v1.1.0

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 06:45

v1.1.0

Release Date: March 3, 2026

Addresses all findings from comprehensive multi-agent code review (5 P1 Critical, 16 P2 Important, 13 P3 Nice-to-Have).

Critical Fixes (P1)

  • Ignore/unignore targeting — Now correctly updates urls.is_ignored, not links table
  • AJAX status poll — Removed batch processing; delegated to WP-Cron to prevent timeout issues
  • Stale scan recovery — 30-minute timeout auto-fails stuck scans
  • Schema version mismatch — Unified version tracking between Plugin.php and Activator.php
  • Ghost property — Fixed $url->redirect_url → $url->final_url

Important Improvements (P2)

  • Parallel HTTP requests — External URLs checked via Requests::request_multiple() for 5-10x faster scans
  • Dashboard optimization — Single GROUP BY query replaces 16+ individual COUNT queries
  • SSRF protection — Blocks requests to private IP ranges (127.x, 10.x, 192.168.x, 169.254.x, fc00::/7)
  • Concurrent batch locks — Transient-based protection prevents duplicate batch processing
  • Race condition fix — TOCTOU handling in find_or_create() URL logic
  • Cascading deletes — Associated links cleaned up on URL deletion
  • Capability checks — Added manage_options fallback for settings access
  • Uninstall cleanup — Fixed stale options list in uninstall.php
  • Exception sanitization — Sensitive data removed from AJAX error responses
  • Persistent cron filter — cron_schedules registered in boot() method
  • JavaScript i18n — All hard-coded English strings localized via wp_localize_script()
  • Timer cleanup — Polling intervals cleared on page unload
  • Streaming CSV — Chunked queries for constant memory usage on large exports
  • Settings cleanup — Removed dead grouped yoko_lc_settings option
  • Post cache priming — N+1 queries eliminated in discovery phase

Code Quality (P3)

  • Deleted unused UrlValidator class (205 LOC)
  • Consolidated 30+ inline debug blocks into Logger calls
  • Removed unused ExtractorRegistry/Interface methods
  • Consolidated duplicate URL-skip logic (3 locations → 1)
  • Injected StatusClassifier into BatchProcessor (proper DI)
  • Removed count method aliases from UrlRepository
  • Removed speculative code for unbuilt features
  • Added composite index (status, is_ignored, id)
  • Primed post caches in LinksListTable
  • Added AJAX response validation in JavaScript
  • Added set_service() for test dependency injection
  • Moved raw SQL from DashboardPage to LinkRepository
  • Wrapped TRUNCATE in transaction with error handling
  • Full PSR-2 coding standards compliance

Files Changed

Category Files
Core yoko-link-checker.php, src/Plugin.php, src/Activator.php
Admin src/Admin/AjaxHandler.php, src/Admin/DashboardPage.php, src/Admin/ResultsPage.php, src/Admin/LinksListTable.php
Scanning src/Scanner/BatchProcessor.php, src/Scanner/ScanOrchestrator.php, src/Scanner/ContentDiscovery.php
Checking src/Checker/UrlChecker.php, src/Checker/CheckResult.php, src/Checker/StatusClassifier.php, src/Checker/HttpClient.php
Repository src/Repository/UrlRepository.php, src/Repository/LinkRepository.php
Utilities src/Util/UrlNormalizer.php, src/Util/Logger.php
Assets assets/js/admin.js
Install uninstall.php

Upgrade Notes

This release includes database schema changes. The plugin will automatically run dbDelta() on update to add the new composite index. No manual action required.

v1.0.8

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 03:28

v1.0.8

Fixed

  • Internal 404s now correctly flagged as broken — URLs that WordPress functions couldn't verify (custom routes, plugin pages, nested archives) were incorrectly marked as "warning" even when they returned 404. Now uses HTTP fallback to get actual status.

Added

  • check_internal_url_via_http() method for fallback HTTP verification of internal URLs
  • yoko_lc_internal_http_args filter for customizing internal HTTP request arguments
  • Short timeout (3 seconds) for internal HTTP checks to prevent delays

Technical Notes

  • WordPress function verification (url_to_postid, taxonomy lookups, etc.) still runs first for performance
  • HTTP fallback only triggers for URLs that can't be resolved via WordPress (~5-10% of internal links)
  • Uses HEAD requests to minimize server load

v1.0.7

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 02:32

1.0.7

Changed

  • Reports page - Renamed from "Broken Links" to "Reports" for broader reporting
  • Link Text - Renamed from "Anchor Text" for better clarity
  • CSV Export - Now includes:
    • Source URL - The actual permalink where the broken link was found
    • Broken URL - The problematic destination URL
    • Source Title - Post/page title containing the link
    • Source Type - Post type (post, page, etc.)
    • Error Details - Specific error message
    • Link Text - The clickable text of the link
    • Last Checked - When the link was last verified

Removed

  • Bulk actions removed to simplify the interface for MVP

v1.0.6

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 02:12

1.0.6

Fixed

  • Fixed TypeError when using bulk actions (Ignore, Un-ignore): LinkRepository::update() expected a Link object but received an integer ID
  • Added update_by_id() method to LinkRepository for updating links by ID with a data array

v1.0.5

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 02:04

1.0.5

Fixed

  • CSV Export now outputs proper CSV data instead of HTML page content
  • Bulk actions (Ignore, Un-ignore, Recheck) now work correctly without white screen errors

Added

  • Status tooltips - hover over any status badge to see a description of what it means:
    • Warning: Server returned a response that may indicate a problem (could be temporary or blocked)
    • Blocked: Destination server blocked the request (common with social media sites)
    • Timeout: Request timed out waiting for response
    • Error: Connection error (DNS, SSL, or network issue)
    • Redirect: URL redirects to a different location
  • Error messages from the server are now shown in status tooltips

v1.0.4

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 01:21

1.0.4

Fixed

  • Export to CSV now works correctly on the results page
  • Clear All Data button now properly removes all scan data

Added

  • CSV export includes URL, status, HTTP code, source post, and anchor text with proper UTF-8 encoding
  • Info notice in scan section reminding users to keep the page open during scans

Changed

  • Refreshed card styling with blue accent border and modern shadow
  • Stat cards now have a subtle hover animation
  • Updated border radius for a cleaner appearance

v1.0.3

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 00:48

v1.0.3 - Internal Link Validation

Properly validates internal URLs for broken links without causing server timeouts.

Fixed

  • Internal URLs now checked - Previously skipped in v1.0.2, now properly validated
  • Detects broken internal links - Finds links to deleted, draft, or unpublished posts

Changed

  • WordPress-native validation - Uses url_to_postid(), get_post(), and term/attachment functions instead of HTTP requests
  • No PHP worker deadlocks - Database queries replace self-referential HTTP calls
  • Unverifiable URLs flagged - Custom routes and archives marked as warnings for manual review

v1.0.2

Choose a tag to compare

@raygunNo raygunNo released this 04 Mar 00:23

v1.0.2 - Production Stability

Fixes critical issues causing timeouts and warnings during link scanning.

Fixed

  • Undefined property warning - Fixed CheckResult::$redirect_url should be $final_url
  • Upstream timeouts - Internal URLs (same domain) were causing PHP worker deadlocks when the server made HTTP requests to itself

Changed

  • Internal URLs auto-validated - Links pointing to the same site are now automatically marked as valid without making HTTP requests
    • Prevents self-referential request blocking on limited-resource hosts (e.g., Kinsta)
    • Internal links are assumed valid since they're part of the same WordPress installation